├── .diagrams └── architecture │ ├── pub-code.drawio │ └── pub-code.drawio.svg ├── .eslintrc.js ├── .github ├── CODEOWNERS ├── ISSUE_TEMPLATE │ ├── bug.md │ ├── custom.md │ ├── decision.md │ ├── documentation.md │ ├── epic.md │ ├── feature.md │ ├── question.md │ ├── schema.md │ ├── task.md │ └── ux.md ├── copilot-instructions.md ├── copilot-upstream.md ├── pull_request_template.md └── workflows │ ├── .deploy.yml │ ├── .tests.yml │ ├── analysis.yml │ ├── merge.yml │ ├── pr-close.yml │ ├── pr-open.yml │ └── scheduled.yml ├── .gitignore ├── .vscode └── settings.json ├── CODE_OF_CONDUCT.md ├── COMPLIANCE.yaml ├── CONTRIBUTING.md ├── LICENSE ├── README.md ├── SECURITY.md ├── api ├── .dockerignore ├── Dockerfile ├── README.md ├── package-lock.json ├── package.json └── src │ ├── app.js │ ├── db │ └── database.js │ ├── email │ ├── ches-service.js │ ├── connection.js │ └── index.js │ ├── entities │ └── pub-code-entity.js │ ├── logger.js │ ├── routes │ └── pubcode-router.js │ ├── schedulers │ └── refresh-cache.js │ ├── server.js │ └── services │ ├── cache-service.js │ └── pub-code-service.js ├── bcgovpubcode.yml ├── charts └── pubcode │ ├── .helmignore │ ├── Chart.lock │ ├── Chart.yaml │ ├── templates │ ├── _helpers.tpl │ ├── knp.yaml │ └── secret.yaml │ └── values.yaml ├── common └── graphics │ ├── deploymentUpdate.png │ ├── main-merge.png │ ├── merge-main.png │ ├── mergeNotification.png │ ├── packages.png │ ├── pr-cleanup.png │ ├── pr-close.png │ ├── pr-open.png │ ├── template.png │ └── unit-tests.png ├── crawler ├── README.md ├── package-lock.json ├── package.json └── src │ └── main.js ├── database └── Dockerfile ├── docker-compose.yml ├── frontend ├── .dockerignore ├── .gitignore ├── Caddyfile ├── Dockerfile ├── README.md ├── _gitignore ├── cypress.config.js ├── cypress │ ├── e2e │ │ ├── edit-form.cy.js │ │ ├── home-page.cy.js │ │ ├── left-drawer.cy.js │ │ └── new-form.cy.js │ └── fixtures │ │ └── example.json ├── env.js ├── eslint.config.js ├── index.html ├── package-lock.json ├── package.json ├── public │ ├── favicon.ico │ └── gov-bc-logo-horiz.ico ├── src │ ├── App.css │ ├── App.jsx │ ├── assets │ │ └── gov-bc-logo-horiz.png │ ├── components │ │ ├── Dashboard.jsx │ │ ├── EditForm.jsx │ │ ├── FormComponent.jsx │ │ ├── LeftDrawer.jsx │ │ ├── NotFound.jsx │ │ ├── PowerBIDashboard.jsx │ │ └── Yaml.jsx │ ├── index.css │ ├── main.jsx │ ├── routes │ │ └── index.jsx │ └── vite-env.d.ts └── vite.config.js ├── renovate.json ├── schema ├── README.md ├── bcgovpubcode.json ├── bcgovpubcode.yml └── script │ ├── index.js │ ├── package-lock.json │ └── package.json ├── sysdig └── sysdig.yml └── utilities └── remove-deleted-pubcode ├── index.js ├── package-lock.json └── package.json /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | env: { 3 | browser: true, 4 | es2021: true, 5 | jest: true, 6 | node: true 7 | }, 8 | extends: [ 9 | 'plugin:import/errors', 10 | 'plugin:import/warnings', 11 | 'plugin:import/typescript', 12 | 'standard' 13 | ], 14 | parser: '@typescript-eslint/parser', 15 | parserOptions: { 16 | ecmaVersion: 'latest', 17 | sourceType: 'module' 18 | }, 19 | plugins: [ 20 | '@typescript-eslint' 21 | ], 22 | rules: { 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /.github/CODEOWNERS: -------------------------------------------------------------------------------- 1 | # Matched against repo root (asterisk) 2 | * @mishraomp @DerekRoberts 3 | 4 | # Matched against directories 5 | # /.github/workflows/ @mishraomp @DerekRoberts 6 | 7 | # See https://docs.github.com/en/repositories/managing-your-repositorys-settings-and-features/customizing-your-repository/about-code-owners 8 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug 3 | about: Create a report to help us improve 4 | title: '' 5 | labels: bug 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Describe the Bug** 11 | A clear and concise description of what the bug is. 12 | 13 | **Expected Behaviour** 14 | A clear and concise description of what you expected to happen. 15 | 16 | **Actual Behaviour** 17 | A clear and concise description of what you expected to happen. 18 | 19 | ** Steps To Reproduce** 20 | Steps to reproduce the behaviour: 21 | 1. Go to '...' 22 | 2. Click on '....' 23 | 3. Scroll down to '....' 24 | 4. See error 25 | 26 | **Screenshots** 27 | If applicable, add screenshots to help explain your problem. 28 | 29 | **Desktop (please complete the following information):** 30 | - OS: [e.g. iOS] 31 | - Browser [e.g. chrome, safari] 32 | - Version [e.g. 22] 33 | 34 | **Additional context** 35 | Add any other context about the problem here. 36 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/custom.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Custom issue template 3 | about: Describe this issue template's purpose here 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/decision.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Decision 3 | about: This is a big decision that has been made or raised to PO 4 | title: '' 5 | labels: decision 6 | assignees: '' 7 | 8 | --- 9 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/documentation.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Documentation 3 | about: Documentation for a specific area or need 4 | title: '' 5 | labels: documentation 6 | assignees: '' 7 | 8 | --- 9 | 10 | **As a** *(User Type/Persona)* **I want** *(Feature/enhancement)* **So That** *(Value, why is this wanted, what is the user trying to accomplish)* 11 | 12 | **Additional Context** 13 | - enter text here 14 | - enter text here 15 | 16 | **Acceptance Criteria** 17 | - [ ] Given (Context), When (action carried out), Then (expected outcome) 18 | - [ ] Given (Context), When (action carried out), Then (expected outcome) 19 | 20 | **Definition of Done** 21 | - [ ] Ready to Demo in Sprint Review 22 | - [ ] Does what I have made have appropriate test coverage? 23 | - [ ] Documentation and/or scientific documentation exists and can be found 24 | - [ ] Peer Reviewed by 2 people on the team 25 | - [ ] Manual testing of all PRs in Dev and Prod 26 | - [ ] Merged 27 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/epic.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Epic 3 | about: A User Story Large enough that it cannot be completed in a single sprint, the 4 | desired end state of a feature 5 | title: '' 6 | labels: epic 7 | assignees: '' 8 | 9 | --- 10 | 11 | **As a** *(User Type/Persona)* **I want** *(Feature/enhancement)* **So That** *(Value, why is this wanted, what is the user trying to accomplish)* 12 | 13 | **Additional Context** 14 | 15 | - enter text here 16 | - enter text here 17 | 18 | **Acceptance Criteria** 19 | 20 | - [ ] Given (Context), When (action carried out), Then (expected outcome) 21 | - [ ] Given (Context), When (action carried out), Then (expected outcome) 22 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request / user story 3 | about: Suggest an idea from the perspective of a user 4 | title: '' 5 | labels: enhancement 6 | assignees: '' 7 | 8 | --- 9 | 10 | **As a** *(User Type/Persona)* **I want** *(Feature/enhancement)* **So That** *(Value, why is this wanted, what is the user trying to accomplish)* 11 | 12 | **Additional Context** 13 | - enter text here 14 | - enter text here 15 | 16 | **Acceptance Criteria** 17 | - [ ] Given (Context), When (action carried out), Then (expected outcome) 18 | - [ ] Given (Context), When (action carried out), Then (expected outcome) 19 | 20 | **Definition of Done** 21 | - [ ] Ready to Demo in Sprint Review 22 | - [ ] Does what I have made have appropriate test coverage? 23 | - [ ] Documentation and/or scientific documentation exists and can be found 24 | - [ ] Peer Reviewed by 2 people on the team 25 | - [ ] Manual testing of all PRs in Dev and Prod 26 | - [ ] Merged 27 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/question.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Question 3 | about: Ask us a question! 4 | title: '' 5 | labels: question 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Describe the task** 11 | basic description of the task, is it focuse on research with users or the business area? is it design focused on either co-design or wireframing? is it User Testing or compiling results? 12 | 13 | **Acceptance Criteria** 14 | - [ ] what is required for this task to be complete? 15 | - what is the finishing point or end state of this task? 16 | - [ ] what is the output of this task? 17 | 18 | **SME/User Contact** 19 | (may want to use a persona to fill this in) 20 | 21 | **Additional context** 22 | - any additional details that could not be captured above 23 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/schema.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Schema change 3 | about: Changes to the schema 4 | title: '' 5 | labels: schema 6 | assignees: '' 7 | 8 | --- 9 | 10 | **As a** *(User Type/Persona)* **I want** *(Feature/enhancement)* **So That** *(Value, why is this wanted, what is the user trying to accomplish)* 11 | 12 | **Additional Context** 13 | - enter text here 14 | - enter text here 15 | 16 | **Acceptance Criteria** 17 | - [ ] Given (Context), When (action carried out), Then (expected outcome) 18 | - [ ] Given (Context), When (action carried out), Then (expected outcome) 19 | 20 | **Definition of Done** 21 | - [ ] Ready to Demo in Sprint Review 22 | - [ ] Does what I have made have appropriate test coverage? 23 | - [ ] Documentation and/or scientific documentation exists and can be found 24 | - [ ] Peer Reviewed by 2 people on the team 25 | - [ ] Manual testing of all PRs in Dev and Prod 26 | - [ ] Merged 27 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/task.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Task 3 | about: Work for the team that cannot be written as a user story 4 | title: '' 5 | labels: task 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Describe the task** 11 | A clear and concise description of what the task is. 12 | 13 | **Acceptance Criteria** 14 | - [ ] first 15 | - [ ] second 16 | - [ ] third 17 | 18 | **Additional context** 19 | - Add any other context about the task here. 20 | - Or here 21 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/ux.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: UX Task 3 | about: This is a Task for UX Research, Design or Testing 4 | title: '' 5 | labels: ux 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Describe the task** 11 | basic description of the task, is it focuse on research with users or the business area? is it design focused on either co-design or wireframing? is it User Testing or compiling results? 12 | 13 | **Acceptance Criteria** 14 | - [ ] what is required for this task to be complete? 15 | - what is the finishing point or end state of this task? 16 | - [ ] what is the output of this task? 17 | 18 | **SME/User Contact** 19 | (may want to use a persona to fill this in) 20 | 21 | **Additional context** 22 | - any additional details that could not be captured above 23 | -------------------------------------------------------------------------------- /.github/copilot-instructions.md: -------------------------------------------------------------------------------- 1 | 5 | 6 | # Project Copilot Instructions 7 | 8 | > For instructions see [copilot-upstream.md](./copilot-upstream.md) 9 | 10 | ## Project Configuration 11 | 12 | **Do not modify `.github/copilot-upstream.md`, which is managed and updated upstream. Instead edit this file for project-specific instructions.** 13 | 14 | Use these technologies: 15 | - Next.js 14 with TypeScript 16 | - PostgreSQL for database 17 | - Node.js >= 18.0.0 18 | - OpenAPI for API design 19 | 20 | Follow these conventions: 21 | - API endpoints: kebab-case 22 | - React components: PascalCase 23 | - Database: Always use parameterized queries 24 | 25 | Project-specific rules: 26 | - Python: 4 spaces (override BC Gov standard) 27 | - APIs: Additional validation for public endpoints 28 | - Branches: feature/JIRA-123-description 29 | 30 | Never: 31 | - Create duplicate files 32 | - Remove existing documentation 33 | - Override error handling 34 | - Bypass security checks 35 | - Generate non-compliant code 36 | - Modify or remove the UPSTREAM MANAGED header at the top of `.github/copilot-upstream.md` 37 | -------------------------------------------------------------------------------- /.github/copilot-upstream.md: -------------------------------------------------------------------------------- 1 | 17 | 18 | You are a coding assistant for BC Government projects. Follow these guidelines: 19 | 20 | When writing code: 21 | - Use 2 spaces for indentation in all files 22 | - Write variables and functions in camelCase 23 | - Keep functions small, focused, and testable 24 | - Add error handling for all async operations 25 | - Follow security guidelines in SECURITY.md 26 | - Include JSDoc comments for functions and classes 27 | - Write unit tests using AAA pattern (Arrange-Act-Assert) 28 | - Preserve existing patterns in the codebase 29 | - Use modern language features appropriately 30 | 31 | For security and compliance: 32 | - Never generate credentials or secrets 33 | - Always validate user inputs 34 | - Use parameterized queries for databases 35 | - Follow BC Government compliance standards 36 | - Add input validation on public endpoints 37 | - Check for performance impacts 38 | - Review generated code for security implications 39 | 40 | When documenting: 41 | - Keep JSDoc comments up to date 42 | - Document complex logic clearly 43 | - Preserve existing documentation structure 44 | - Include usage examples for APIs 45 | - Use consistent Markdown formatting 46 | 47 | Never: 48 | - Create duplicate files 49 | - Remove existing documentation 50 | - Override error handling 51 | - Bypass security checks 52 | - Generate non-compliant code 53 | -------------------------------------------------------------------------------- /.github/pull_request_template.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | # Description 4 | 5 | Please provide a summary of the change and the issue fixed. Please include relevant context. List dependency changes. 6 | 7 | Fixes # (issue) 8 | 9 | ## Type of change 10 | 11 | Please delete options that are not relevant. 12 | 13 | - [ ] Bug fix (non-breaking change which fixes an issue) 14 | - [ ] New feature (non-breaking change which adds functionality) 15 | - [ ] Breaking change (fix or feature that would cause existing functionality to not work as expected) 16 | - [ ] This change requires a documentation update 17 | 18 | # How Has This Been Tested? 19 | 20 | Please describe the tests that you ran to verify your changes. Provide instructions so we can reproduce. Please also list any relevant details for your test configuration 21 | 22 | - [ ] Test A 23 | - [ ] Test B 24 | 25 | 26 | ## Checklist 27 | 28 | 29 | 30 | 31 | - [ ] I have read the [CONTRIBUTING](CONTRIBUTING.md) doc 32 | - [ ] I have performed a self-review of my own code 33 | - [ ] I have commented my code, particularly in hard-to-understand areas 34 | - [ ] I have made corresponding changes to the documentation 35 | - [ ] My changes generate no new warnings 36 | - [ ] I have added tests that prove my fix is effective or that my feature works 37 | - [ ] New and existing unit tests pass locally with my changes 38 | - [ ] Any dependent changes have already been accepted and merged 39 | 40 | 41 | ## Further comments 42 | 43 | 44 | -------------------------------------------------------------------------------- /.github/workflows/.deploy.yml: -------------------------------------------------------------------------------- 1 | name: .Deploys 2 | 3 | on: 4 | workflow_call: 5 | inputs: 6 | ### Typical / recommended 7 | autoscaling: 8 | description: Autoscaling enabled or not for the deployments 9 | required: false 10 | type: string 11 | default: 'true' 12 | environment: 13 | description: Environment name; omit for PRs 14 | required: false 15 | type: string 16 | tag: 17 | description: Container tag; usually PR number 18 | required: false 19 | type: string 20 | default: ${{ github.event.number }} 21 | triggers: 22 | description: Paths to trigger a deploy; omit=always; e.g. ('backend/' 'frontend/') 23 | required: false 24 | type: string 25 | 26 | ### Usually a bad idea / not recommended 27 | directory: 28 | description: "Chart directory" 29 | default: "charts/${{ github.event.repository.name }}" 30 | required: false 31 | type: string 32 | timeout-minutes: 33 | description: "Timeout minutes" 34 | default: 10 35 | required: false 36 | type: number 37 | values: 38 | description: "Values file" 39 | default: "values.yaml" 40 | required: false 41 | type: string 42 | params: 43 | description: "Extra parameters to pass to helm upgrade" 44 | default: "" 45 | required: false 46 | type: string 47 | release_name: 48 | description: "Release name" 49 | default: ${{ github.event.repository.name }} 50 | required: false 51 | type: string 52 | 53 | 54 | jobs: 55 | 56 | deploys: 57 | name: Helm 58 | environment: ${{ inputs.environment }} 59 | runs-on: ubuntu-24.04 60 | timeout-minutes: ${{ inputs.timeout-minutes }} 61 | steps: 62 | - uses: actions/checkout@v4 63 | - name: Stop pre-existing deployments on PRs (status = pending-upgrade) 64 | if: github.event_name == 'pull_request' 65 | uses: bcgov/action-oc-runner@10033668ef4374d9bb78149faa73e4ccda0e93dd # v1.2.3 66 | with: 67 | oc_namespace: ${{ vars.oc_namespace }} 68 | oc_token: ${{ secrets.oc_token }} 69 | oc_server: ${{ vars.oc_server }} 70 | triggers: ${{ inputs.triggers }} 71 | commands: | 72 | # Interrupt any previous deployments (PR only) 73 | PREVIOUS=$(helm status ${{ inputs.release_name }} -o json | jq .info.status || true) 74 | if [[ ${PREVIOUS} =~ pending ]]; then 75 | echo "Rollback triggered" 76 | helm rollback ${{ inputs.release_name }} || \ 77 | helm uninstall ${{ inputs.release_name }} 78 | fi 79 | 80 | - name: Deploy 81 | uses: bcgov/action-oc-runner@10033668ef4374d9bb78149faa73e4ccda0e93dd # v1.2.3 82 | with: 83 | oc_namespace: ${{ vars.oc_namespace }} 84 | oc_token: ${{ secrets.oc_token }} 85 | oc_server: ${{ vars.oc_server }} 86 | triggers: ${{ inputs.triggers }} 87 | commands: | 88 | # echo current git branch 89 | 90 | # If directory provided, cd to it 91 | [ -z "${{ inputs.directory }}" ]|| cd ${{ inputs.directory }} 92 | # Deploy Helm Chart 93 | helm dependency update 94 | helm package --app-version="v${{ inputs.tag }}" --version=${{ inputs.tag }} . 95 | cp ./${{ github.event.repository.name }}-${{ inputs.tag }}.tgz pubcode.tgz 96 | COMMANDS="--install --wait --atomic --timeout ${{ inputs.timeout-minutes }}m" 97 | PARAMS="${{ inputs.release_name }} \ 98 | --set global.autoscaling=${{ inputs.autoscaling }} \ 99 | --set-string global.repository=${{ github.repository }} \ 100 | --set-string global.secrets.emailRecipients=${{ secrets.EMAIL_RECIPIENTS }} \ 101 | --set-string global.secrets.chesTokenURL=${{ secrets.CHES_TOKEN_URL }} \ 102 | --set-string global.secrets.chesClientID=${{ secrets.CHES_CLIENT_ID }} \ 103 | --set-string global.secrets.chesClientSecret=${{ secrets.CHES_CLIENT_SECRET }} \ 104 | --set-string global.secrets.chesAPIURL=${{ secrets.CHES_API_URL }} \ 105 | --set-string global.secrets.databaseAdminPassword=${{ secrets.DB_PWD }} \ 106 | --set-string global.secrets.powerBIURL=${{ secrets.POWERBI_URL }} \ 107 | --values ${{ inputs.values }} " 108 | 109 | if [ -n "${{ inputs.params }}" ]; then 110 | PARAMS+="${{ inputs.params }}" 111 | fi 112 | echo "PARAMS: $PARAMS" 113 | echo "COMMANDS: $COMMANDS" 114 | helm upgrade $PARAMS $COMMANDS pubcode.tgz 115 | #helm install --dry-run --debug $PARAMS pubcode.tgz #for debugging 116 | 117 | # print history 118 | helm history ${{ inputs.release_name }} || true 119 | 120 | # Remove old build runs, build pods and deployment pods 121 | oc delete po --field-selector=status.phase==Succeeded 122 | -------------------------------------------------------------------------------- /.github/workflows/.tests.yml: -------------------------------------------------------------------------------- 1 | name: .Tests 2 | 3 | on: 4 | workflow_call: 5 | inputs: 6 | ### Required 7 | target: 8 | description: PR number, test or prod 9 | required: true 10 | type: string 11 | 12 | jobs: 13 | cypress-e2e: 14 | name: Cypress E2E 15 | runs-on: ubuntu-24.04 16 | strategy: 17 | matrix: 18 | browser: [chrome] 19 | steps: 20 | - uses: actions/checkout@v4 21 | - uses: cypress-io/github-action@v5 22 | name: Cypress run 23 | with: 24 | config: pageLoadTimeout=30000,baseUrl=https://pubcode-${{ inputs.target }}.apps.silver.devops.gov.bc.ca/ 25 | working-directory: ./frontend 26 | browser: ${{ matrix.browser }} 27 | - uses: actions/upload-artifact@v4 28 | if: failure() 29 | with: 30 | name: cypress-screenshots 31 | path: ./frontend/cypress/screenshots 32 | if-no-files-found: ignore # 'warn' or 'error' are also available, defaults to `warn` 33 | -------------------------------------------------------------------------------- /.github/workflows/analysis.yml: -------------------------------------------------------------------------------- 1 | name: Analysis 2 | 3 | on: 4 | push: 5 | branches: [main] 6 | merge_group: 7 | pull_request: 8 | types: [opened, reopened, synchronize, ready_for_review] 9 | schedule: 10 | - cron: "0 12 * * 0" # 3 AM PST = 12 PM UDT, runs sundays 11 | workflow_dispatch: 12 | 13 | concurrency: 14 | group: ${{ github.workflow }}-${{ github.ref }} 15 | cancel-in-progress: true 16 | 17 | jobs: 18 | # https://github.com/marketplace/actions/aqua-security-trivy 19 | trivy: 20 | name: Trivy Security Scan 21 | if: github.event_name != 'pull_request' || !github.event.pull_request.draft 22 | runs-on: ubuntu-24.04 23 | steps: 24 | - uses: actions/checkout@v4 25 | 26 | - name: Run Trivy vulnerability scanner in repo mode 27 | uses: aquasecurity/trivy-action@76071ef0d7ec797419534a183b498b4d6366cf37 # 0.31.0 28 | with: 29 | format: "sarif" 30 | output: "trivy-results.sarif" 31 | ignore-unfixed: true 32 | scan-type: "fs" 33 | scanners: "vuln,secret,config" 34 | severity: "CRITICAL,HIGH" 35 | 36 | - name: Upload Trivy scan results to GitHub Security tab 37 | uses: github/codeql-action/upload-sarif@v3 38 | with: 39 | sarif_file: "trivy-results.sarif" 40 | -------------------------------------------------------------------------------- /.github/workflows/merge.yml: -------------------------------------------------------------------------------- 1 | name: Merge to Main 2 | 3 | on: 4 | workflow_run: 5 | workflows: [Pull Request Closed] 6 | types: [completed] 7 | workflow_dispatch: 8 | 9 | concurrency: 10 | group: ${{ github.workflow }} 11 | cancel-in-progress: true 12 | 13 | jobs: 14 | semantic-version: 15 | outputs: 16 | semanticVersion: ${{ steps.changelog.outputs.version }} # this is without v 1.0.0 17 | tag: ${{ steps.changelog.outputs.tag }} # this is with v, v1.0.0 18 | clean_changelog: ${{ steps.changelog.outputs.clean_changelog }} 19 | runs-on: ubuntu-24.04 20 | timeout-minutes: 1 21 | steps: 22 | - uses: actions/checkout@v4 23 | - name: Conventional Changelog Update 24 | uses: TriPSs/conventional-changelog-action@67139193614f5b9e8db87da1bd4240922b34d765 # v6.0.0 25 | id: changelog 26 | with: 27 | github-token: ${{ github.token }} 28 | output-file: 'CHANGELOG.md' 29 | skip-version-file: 'true' 30 | skip-commit: 'true' 31 | skip-on-empty: 'false' 32 | git-push: 'true' 33 | 34 | retag-images: 35 | needs: [ semantic-version ] 36 | runs-on: ubuntu-24.04 37 | strategy: 38 | matrix: 39 | package: [ api, database, frontend ] 40 | steps: 41 | - name: Tag Docker Images 42 | uses: shrink/actions-docker-registry-tag@f04afd0559f66b288586792eb150f45136a927fa # v4 43 | with: 44 | registry: ghcr.io 45 | repository: ${{ github.repository }}/${{ matrix.package }} 46 | target: latest 47 | tags: | 48 | ${{ needs.semantic-version.outputs.semanticVersion }} 49 | ${{ needs.semantic-version.outputs.tag }} 50 | 51 | deploys: 52 | name: TEST Deployments 53 | needs: [retag-images, semantic-version] 54 | uses: ./.github/workflows/.deploy.yml 55 | secrets: inherit 56 | with: 57 | autoscaling: true 58 | environment: test 59 | tag: ${{ needs.semantic-version.outputs.semanticVersion }} # this is without v 60 | release_name: pubcode-test 61 | params: --set-string api.containers[0].tag="${{ needs.semantic-version.outputs.tag }}" --set-string frontend.containers[0].tag="${{ needs.semantic-version.outputs.tag }}" 62 | tests: 63 | name: Tests 64 | needs: [deploys] 65 | uses: ./.github/workflows/.tests.yml 66 | with: 67 | target: test 68 | 69 | deploys-prod: 70 | name: PROD Deployments 71 | needs: [semantic-version, tests] 72 | uses: ./.github/workflows/.deploy.yml 73 | secrets: inherit 74 | with: 75 | autoscaling: true 76 | environment: prod 77 | tag: ${{ needs.semantic-version.outputs.semanticVersion }} 78 | release_name: pubcode 79 | params: --set-string api.containers[0].tag="${{ needs.semantic-version.outputs.tag }}" --set-string frontend.containers[0].tag="${{ needs.semantic-version.outputs.tag }}" 80 | github_release: 81 | name: Create Release 82 | needs: [semantic-version, deploys-prod] 83 | runs-on: ubuntu-24.04 84 | steps: 85 | - name: Create Release 86 | uses: softprops/action-gh-release@72f2c25fcb47643c292f7107632f7a47c1df5cd8 # v2 87 | continue-on-error: true 88 | env: 89 | GITHUB_TOKEN: ${{ github.token }} 90 | with: 91 | token: ${{ github.token }} 92 | tag_name: ${{ needs.semantic-version.outputs.tag }} 93 | name: ${{ needs.semantic-version.outputs.tag }} 94 | body: ${{ needs.semantic-version.outputs.clean_changelog }} 95 | -------------------------------------------------------------------------------- /.github/workflows/pr-close.yml: -------------------------------------------------------------------------------- 1 | name: Pull Request Closed 2 | 3 | on: 4 | pull_request: 5 | types: 6 | - closed 7 | 8 | concurrency: 9 | group: ${{ github.workflow }}-${{ github.ref }} 10 | cancel-in-progress: true 11 | 12 | jobs: 13 | cleanup: 14 | name: Cleanup and Image Promotion 15 | uses: bcgov/quickstart-openshift-helpers/.github/workflows/.pr-close.yml@0b8121a528aaa05ef8def2f79be9081691dfe98a # v0.9.0 16 | permissions: 17 | packages: write 18 | secrets: 19 | oc_namespace: ${{ vars.OC_NAMESPACE }} 20 | oc_token: ${{ secrets.OC_TOKEN }} 21 | with: 22 | cleanup: helm 23 | packages: api frontend 24 | 25 | -------------------------------------------------------------------------------- /.github/workflows/pr-open.yml: -------------------------------------------------------------------------------- 1 | name: Pull Request 2 | 3 | on: 4 | pull_request: 5 | merge_group: 6 | workflow_dispatch: 7 | 8 | concurrency: 9 | group: ${{ github.workflow }}-${{ github.ref }} 10 | cancel-in-progress: true 11 | 12 | jobs: 13 | pr-greeting: 14 | name: PR Greeting 15 | env: 16 | DOMAIN: apps.silver.devops.gov.bc.ca 17 | PREFIX: ${{ github.event.repository.name }}-${{ github.event.number }} 18 | runs-on: ubuntu-24.04 19 | permissions: 20 | pull-requests: write 21 | steps: 22 | - name: PR Greeting 23 | uses: bcgov/action-pr-description-add@14338bfe0278ead273b3c1189e5aa286ff6709c4 # v2.0.0 24 | with: 25 | github_token: ${{ secrets.GITHUB_TOKEN }} 26 | add_markdown: | 27 | --- 28 | 29 | Thanks for the PR! 30 | 31 | Any successful deployments (not always required) will be available below. 32 | [API](https://${{ env.PREFIX }}-api.${{ env.DOMAIN }}/) available 33 | [Frontend](https://${{ env.PREFIX }}.${{ env.DOMAIN }}/) available 34 | 35 | Once merged, code will be promoted and handed off to following workflow run. 36 | [Main Merge Workflow](https://github.com/${{ github.repository }}/actions/workflows/merge-main.yml) 37 | 38 | builds: 39 | name: Builds 40 | runs-on: ubuntu-24.04 41 | permissions: 42 | attestations: write 43 | id-token: write 44 | packages: write 45 | strategy: 46 | matrix: 47 | package: [api, frontend] 48 | steps: 49 | - uses: actions/checkout@v4 50 | - uses: bcgov/action-builder-ghcr@fd17bc1cbb16a60514e0df3966d42dff9fc232bc # v4.0.0 51 | with: 52 | package: ${{ matrix.package }} 53 | tags: ${{ github.event.number || github.sha }} 54 | tag_fallback: latest 55 | token: ${{ secrets.GITHUB_TOKEN }} 56 | 57 | - uses: shrink/actions-docker-registry-tag@f04afd0559f66b288586792eb150f45136a927fa # v4 58 | with: 59 | registry: ghcr.io 60 | repository: ${{ github.repository }}/${{ matrix.package }} 61 | target: ${{ github.event.number || github.sha }} 62 | tags: ${{ github.sha }} 63 | 64 | deploys: 65 | name: Deploys 66 | needs: [builds] 67 | uses: ./.github/workflows/.deploy.yml 68 | secrets: inherit 69 | with: 70 | autoscaling: false 71 | tag: ${{ github.event.number }} 72 | release_name: pubcode-${{ github.event.number }} 73 | params: | 74 | --set-string global.repository=${{ github.repository }} \ 75 | --set-string api.containers[0].tag="${{ github.sha }}" \ 76 | --set-string api.containers[0].resources.requests.cpu="30m" \ 77 | --set-string api.containers[0].resources.requests.memory="50Mi" \ 78 | --set-string frontend.containers[0].tag="${{ github.sha }}" \ 79 | --set-string frontend.containers[0].resources.requests.cpu="30m" \ 80 | --set-string frontend.containers[0].resources.requests.memory="50Mi" \ 81 | --set-string database.containers[0].resources.requests.cpu="30m" \ 82 | --set-string database.containers[0].resources.requests.memory="50Mi" \ 83 | --set-string database.initContainers[0].resources.requests.cpu="30m" \ 84 | --set-string database.initContainers[0].resources.requests.memory="50Mi" \ 85 | --set-string database.pvc.size="350Mi" \ 86 | --set-string global.env.VITE_SCHEMA_BRANCH=${{ github.event.pull_request.head.ref }} \ 87 | 88 | tests: 89 | name: Tests 90 | needs: [deploys] 91 | uses: ./.github/workflows/.tests.yml 92 | with: 93 | target: ${{ github.event.number }} 94 | -------------------------------------------------------------------------------- /.github/workflows/scheduled.yml: -------------------------------------------------------------------------------- 1 | name: Schedule Jobs 2 | 3 | on: 4 | workflow_dispatch: 5 | inputs: 6 | REPO_NAMES: 7 | required: false 8 | description: comma separated list of repo names within bcgov org. for one of jobs to run for specific repos. 9 | schedule: # * is a special character in YAML, so you have to quote this string, every day at 4PM PST as 5PM the powerbi refresh happens. 10 | - cron: "0 0 * * *" 11 | 12 | concurrency: 13 | group: ${{ github.workflow }} 14 | cancel-in-progress: true 15 | 16 | jobs: 17 | pubcode-crawler: 18 | name: Crawl Git Repos for bcgovpubcode.yml 19 | runs-on: ubuntu-24.04 20 | environment: prod 21 | defaults: 22 | run: 23 | working-directory: crawler 24 | steps: 25 | - name: Check out repository 26 | uses: actions/checkout@v4 27 | 28 | - name: Add Node.js 29 | uses: actions/setup-node@v4 30 | with: 31 | node-version: "22.x" 32 | - name: Install Dependencies 33 | run: npm ci 34 | 35 | - uses: actions/cache@v4 36 | with: 37 | path: ~/.npm 38 | key: ${{ runner.os }}-node-${{ hashFiles('**/package-lock.json') }} 39 | restore-keys: | 40 | ${{ runner.os }}-node- 41 | 42 | - uses: redhat-actions/openshift-tools-installer@144527c7d98999f2652264c048c7a9bd103f8a82 # v1 43 | with: 44 | oc: "4" 45 | 46 | - name: Process script 47 | env: 48 | GIT_TOKEN: ${{ secrets.GITHUB_TOKEN }} 49 | REPO_NAMES: ${{ github.event.inputs.REPO_NAMES }} 50 | run: | 51 | oc login --token=${{ secrets.OC_TOKEN }} --server=${{ vars.OC_SERVER }} 52 | oc project ${{ vars.OC_NAMESPACE }} 53 | 54 | # Get API key 55 | API_KEY=$(oc get secrets/pubcode --template={{.data.API_KEY}} | base64 -d) 56 | API_URL=https://$(oc get route/pubcode-api --template={{.spec.host}}) 57 | API_KEY="${API_KEY}" API_URL="${API_URL}" node src/main.js 58 | 59 | validate-ministry-list: 60 | name: Validate Ministry List in the pubcode schema. 61 | runs-on: ubuntu-24.04 62 | defaults: 63 | run: 64 | working-directory: schema/script 65 | steps: 66 | - name: Check out repository 67 | uses: actions/checkout@v4 68 | 69 | - name: Add Node.js 70 | uses: actions/setup-node@v4 71 | with: 72 | node-version: "22.x" 73 | 74 | - name: Install Dependencies 75 | run: npm ci 76 | 77 | - uses: actions/cache@v4 78 | with: 79 | path: ~/.npm 80 | key: ${{ runner.os }}-node-${{ hashFiles('**/package-lock.json') }} 81 | restore-keys: | 82 | ${{ runner.os }}-node- 83 | 84 | - name: Process script 85 | id: validate-ministry-list 86 | run: node ./index.js 87 | 88 | - name: create a branch, commit and push changes 89 | if: steps.validate-ministry-list.outputs.schemaChanged == 'true' 90 | env: 91 | GH_TOKEN: ${{ github.token }} 92 | run: | 93 | git config --local user.name ${{ github.actor }} 94 | git checkout -b chore/ministry-name-schema 95 | git add ../bcgovpubcode.json 96 | git commit -m "Updating the Schema as changes to ministry names were detected." 97 | git push origin chore/ministry-name-schema 98 | # Create a Pull Request 99 | gh pr create --assignee "mishraomp" --base main --label "chore" --title "Updating the Schema as changes to ministry names were detected." --body "Updating the Schema as changes to ministry names were detected." 100 | 101 | soft-delete-removed-pubcodes: 102 | name: Soft Delete pubcodes In the Databse which are removed from the repo. 103 | runs-on: ubuntu-24.04 104 | defaults: 105 | run: 106 | working-directory: utilities/remove-deleted-pubcode 107 | environment: prod 108 | steps: 109 | - name: Check out repository 110 | uses: actions/checkout@v4 111 | 112 | - name: Add Node.js 113 | uses: actions/setup-node@v4 114 | with: 115 | node-version: "22.x" 116 | - name: Install Dependencies 117 | run: npm ci 118 | 119 | - uses: actions/cache@v4 120 | with: 121 | path: ~/.npm 122 | key: ${{ runner.os }}-node-${{ hashFiles('**/package-lock.json') }} 123 | restore-keys: | 124 | ${{ runner.os }}-node- 125 | 126 | - uses: redhat-actions/openshift-tools-installer@144527c7d98999f2652264c048c7a9bd103f8a82 # v1 127 | with: 128 | oc: "4" 129 | 130 | - name: Process script 131 | run: | 132 | oc login --token=${{ secrets.OC_TOKEN }} --server=${{ vars.OC_SERVER }} 133 | oc project ${{ vars.OC_NAMESPACE }} 134 | 135 | # Get API key 136 | API_KEY=$(oc get secrets/pubcode --template={{.data.API_KEY}} | base64 -d) 137 | API_URL=https://$(oc get route/pubcode-api --template={{.spec.host}}) 138 | API_KEY="${API_KEY}" API_URL="${API_URL}" node index.js 139 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | lerna-debug.log* 8 | 9 | # Diagnostic reports (https://nodejs.org/api/report.html) 10 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json 11 | 12 | # Runtime data 13 | pids 14 | *.pid 15 | *.seed 16 | *.pid.lock 17 | 18 | # Directory for instrumented libs generated by jscoverage/JSCover 19 | lib-cov 20 | 21 | # Coverage directory used by tools like istanbul 22 | coverage 23 | *.lcov 24 | lcov.* 25 | 26 | # nyc test coverage 27 | .nyc_output 28 | 29 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) 30 | .grunt 31 | 32 | # Bower dependency directory (https://bower.io/) 33 | bower_components 34 | 35 | # node-waf configuration 36 | .lock-wscript 37 | 38 | # Compiled binary addons (https://nodejs.org/api/addons.html) 39 | build/Release 40 | 41 | # Dependency directories 42 | node_modules/ 43 | jspm_packages/ 44 | 45 | # TypeScript v1 declaration files 46 | typings/ 47 | 48 | # TypeScript cache 49 | *.tsbuildinfo 50 | 51 | # Optional npm cache directory 52 | .npm 53 | 54 | # Optional eslint cache 55 | .eslintcache 56 | 57 | # Microbundle cache 58 | .rpt2_cache/ 59 | .rts2_cache_cjs/ 60 | .rts2_cache_es/ 61 | .rts2_cache_umd/ 62 | 63 | # Optional REPL history 64 | .node_repl_history 65 | 66 | # Output of 'npm pack' 67 | *.tgz 68 | 69 | # Yarn Integrity file 70 | .yarn-integrity 71 | 72 | # dotenv environment variables file 73 | .env 74 | .env.test 75 | 76 | # parcel-bundler cache (https://parceljs.org/) 77 | .cache 78 | 79 | # Next.js build output 80 | .next 81 | 82 | # Nuxt.js build / generate output 83 | .nuxt 84 | dist 85 | 86 | # Gatsby files 87 | .cache/ 88 | # Comment in the public line in if your project uses Gatsby and *not* Next.js 89 | # https://nextjs.org/blog/next-9-1#public-directory-support 90 | # public 91 | 92 | # vuepress build output 93 | .vuepress/dist 94 | 95 | # Serverless directories 96 | .serverless/ 97 | 98 | # FuseBox cache 99 | .fusebox/ 100 | 101 | # DynamoDB Local files 102 | .dynamodb/ 103 | 104 | # TernJS port file 105 | .tern-port 106 | 107 | # Jest 108 | test-report.xml 109 | 110 | # Docker/Podman volumes 111 | .volumes 112 | frontend/.env.local 113 | .idea 114 | /util/ 115 | *.http -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "github.copilot.chat.codeGeneration.useInstructionFiles": true, 3 | "github.copilot.chat.codeGeneration.instructions": [ 4 | { 5 | "file": ".github/copilot-upstream.md" 6 | } 7 | ] 8 | } 9 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Overview 4 | 5 | Act in the best interests of the community, the government of British Columbia and your fellow collaborators. We welcome and appreciate your contributions, in any capacity. 6 | 7 | ## Our Pledge 8 | 9 | We as members, contributors, and leaders pledge to make participation in our 10 | community a harassment-free experience for everyone, regardless of age, body 11 | size, visible or invisible disability, ethnicity, sex characteristics, gender 12 | identity and expression, level of experience, education, socio-economic status, 13 | nationality, personal appearance, race, religion, or sexual identity 14 | and orientation. 15 | 16 | We pledge to act and interact in ways that contribute to an open, welcoming, 17 | diverse, inclusive, and healthy community. 18 | 19 | ## Our Standards 20 | 21 | Examples of behavior that contributes to a positive environment for our 22 | community include: 23 | 24 | * Demonstrating empathy and kindness toward other people 25 | * Being respectful of differing opinions, viewpoints, and experiences 26 | * Giving and gracefully accepting constructive feedback 27 | * Accepting responsibility and apologizing to those affected by our mistakes, 28 | and learning from the experience 29 | * Focusing on what is best not just for us as individuals, but for the 30 | overall community 31 | 32 | Examples of unacceptable behavior include: 33 | 34 | * The use of sexualized language or imagery, and sexual attention or 35 | advances of any kind 36 | * Trolling, insulting or derogatory comments, and personal or political attacks 37 | * Public or private harassment 38 | * Publishing others' private information, such as a physical or email 39 | address, without their explicit permission 40 | * Other conduct which could reasonably be considered inappropriate in a 41 | professional setting 42 | 43 | ## Enforcement Responsibilities 44 | 45 | Community leaders are responsible for clarifying and enforcing our standards of 46 | acceptable behavior and will take appropriate and fair corrective action in 47 | response to any behavior that they deem inappropriate, threatening, offensive, 48 | or harmful. 49 | 50 | Community leaders have the right and responsibility to remove, edit, or reject 51 | comments, commits, code, wiki edits, issues, and other contributions that are 52 | not aligned to this Code of Conduct, and will communicate reasons for moderation 53 | decisions when appropriate. 54 | 55 | ## Scope 56 | 57 | This Code of Conduct applies within all community spaces, and also applies when 58 | an individual is officially representing the community in public spaces. 59 | Examples of representing our community include using an official e-mail address, 60 | posting via an official social media account, or acting as an appointed 61 | representative at an online or offline event. 62 | 63 | ## Enforcement 64 | 65 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 66 | reported to the community leaders responsible for enforcement at 67 | derek.roberts@gmail.com. 68 | All complaints will be reviewed and investigated promptly and fairly. 69 | 70 | All community leaders are obligated to respect the privacy and security of the 71 | reporter of any incident. 72 | 73 | ## Enforcement Guidelines 74 | 75 | Community leaders will follow these Community Impact Guidelines in determining 76 | the consequences for any action they deem in violation of this Code of Conduct: 77 | 78 | ### 1. Correction 79 | 80 | **Community Impact**: Use of inappropriate language or other behavior deemed 81 | unprofessional or unwelcome in the community. 82 | 83 | **Consequence**: A private, written warning from community leaders, providing 84 | clarity around the nature of the violation and an explanation of why the 85 | behavior was inappropriate. A public apology may be requested. 86 | 87 | ### 2. Warning 88 | 89 | **Community Impact**: A violation through a single incident or series 90 | of actions. 91 | 92 | **Consequence**: A warning with consequences for continued behavior. No 93 | interaction with the people involved, including unsolicited interaction with 94 | those enforcing the Code of Conduct, for a specified period of time. This 95 | includes avoiding interactions in community spaces as well as external channels 96 | like social media. Violating these terms may lead to a temporary or 97 | permanent ban. 98 | 99 | ### 3. Temporary Ban 100 | 101 | **Community Impact**: A serious violation of community standards, including 102 | sustained inappropriate behavior. 103 | 104 | **Consequence**: A temporary ban from any sort of interaction or public 105 | communication with the community for a specified period of time. No public or 106 | private interaction with the people involved, including unsolicited interaction 107 | with those enforcing the Code of Conduct, is allowed during this period. 108 | Violating these terms may lead to a permanent ban. 109 | 110 | ### 4. Permanent Ban 111 | 112 | **Community Impact**: Demonstrating a pattern of violation of community 113 | standards, including sustained inappropriate behavior, harassment of an 114 | individual, or aggression toward or disparagement of classes of individuals. 115 | 116 | **Consequence**: A permanent ban from any sort of public interaction within 117 | the community. 118 | 119 | ## Attribution 120 | 121 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], 122 | version 2.0, available at 123 | https://www.contributor-covenant.org/version/2/0/code_of_conduct.html. 124 | 125 | Community Impact Guidelines were inspired by [Mozilla's code of conduct 126 | enforcement ladder](https://github.com/mozilla/diversity). 127 | 128 | [homepage]: https://www.contributor-covenant.org 129 | 130 | For answers to common questions about this code of conduct, see the FAQ at 131 | https://www.contributor-covenant.org/faq. Translations are available at 132 | https://www.contributor-covenant.org/translations. 133 | -------------------------------------------------------------------------------- /COMPLIANCE.yaml: -------------------------------------------------------------------------------- 1 | name: compliance 2 | description: | 3 | This document is used to track a projects PIA and STRA 4 | compliance. 5 | spec: 6 | - name: PIA 7 | status: not-required 8 | last-updated: '2022-01-26T23:07:19.992Z' 9 | - name: STRA 10 | status: not-required 11 | last-updated: '2022-01-26T23:07:19.992Z' 12 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # How to contribute 2 | 3 | Government employees, public and members of the private sector are encouraged to contribute to the repository by **forking and submitting a pull request**. 4 | 5 | (If you are new to GitHub, you might start with a [basic tutorial](https://help.github.com/articles/set-up-git) and check out a more detailed guide to [pull requests](https://help.github.com/articles/using-pull-requests/).) 6 | 7 | Pull requests will be evaluated by the repository guardians on a schedule and if deemed beneficial will be committed to the main branch. 8 | 9 | All contributors retain the original copyright to their stuff, but by contributing to this project, you grant a world-wide, royalty-free, perpetual, irrevocable, non-exclusive, transferable license to all users **under the terms of the [license](./LICENSE.md) under which this project is distributed**. 10 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "{}" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright {yyyy} {name of copyright owner} 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | [![Issues](https://img.shields.io/github/issues/bcgov/pubcode)](/../../issues) 5 | [![Pull Requests](https://img.shields.io/github/issues-pr/bcgov/pubcode)](/../../pulls) 6 | [![MIT License](https://img.shields.io/github/license/bcgov/pubcode.svg)](/LICENSE.md) 7 | [![Lifecycle](https://img.shields.io/badge/Lifecycle-Experimental-339999)](https://github.com/bcgov/repomountie/blob/master/doc/lifecycle-badges.md) 8 | 9 | 10 | # BCGov public code asset tracking 11 | 12 | This contains schema definitions for yml and a web application to assist end users to create/edit bcgovpubcode.yml files, store them in a MongoDB database and retrieve them using a Node/Express API. 13 | 14 | ## Architecture 15 | ![Pub Code Architecture](.diagrams/architecture/pub-code.drawio.svg) 16 | 17 | 18 | ## FAQ 19 | Please click [here](https://github.com/bcgov/pubcode/wiki/Frequently-Asked-Questions) 20 | 21 | ## Components 22 | 23 | 1. Database (MongoDB): Stores all bcgovpubcode.yml files, converted to JSON, for each participating repo in the bcgov organization. 24 | 2. Backend (Node/Express API): Provides read and write access to the database. The read endpoint is open to the public, while the write endpoint is protected by an API key. Both are rate-limited. 25 | 3. Crawler (Node module): Crawls through GitHub repos on a schedule, collects any bcgovpubcode.yml files, converts to JSON, and stores using the API's write endpoint. 26 | 4. Frontend (React.js and MaterialUI): Allows users to create bcgovpubcode.yml files or edit existing ones using a GitHub link. 27 | 5. Schema (JSON Schema): The standard on which the bcgovpubcode.yml file is based. 28 | 29 | ## How it works 30 | 31 | 1. The Crawler runs in a [scheduled GitHub Action](.github/workflows/pubcode-crawler-on-scheduler.yml), collecting bcgovpubcode.yml files from participating GitHub repositories. 32 | 2. Yaml is converted to JSON and sent to the API's write endpoint, which is secured with an API key. 33 | 3. The API stores to the MongoDB database. 34 | 4. Users can access all data from the API's read endpoint. 35 | 5. Frontend allows users to create or edit bcgovpubcode.yml files, which are validated against the [JSON schema](schema/bcgovpubcode.json). 36 | 37 | ## Note 38 | 39 | Only the Crawler module is allowed to call the APIs post endpoint, which uses the MongoDB as its source of truth. 40 | 41 | ```mermaid 42 | sequenceDiagram 43 | participant Crawler 44 | participant API 45 | participant MongoDB 46 | participant Frontend 47 | participant JSON Schema 48 | participant GitHub Repos 49 | 50 | Crawler->>GitHub Repos: Crawls through 51 | GitHub Repos->>Crawler: Collects bcgovpubcode.yml files 52 | Crawler->>API: Calls the secured post endpoint 53 | API->>MongoDB: Stores JSON data 54 | Frontend->>JSON Schema: Uses as standard to create or edit bcgovpubcode.yml files 55 | ``` 56 | 57 | ## More Information 58 | 59 | Please see README.md files in each component's folder to learn more. 60 | 61 | [api/README.md](api/README.md) 62 | 63 | [schema/README.md](schema/README.md) 64 | 65 | [frontend/README.md](frontend/README.md) 66 | 67 | [crawler/README.md](crawler/README.md) 68 | -------------------------------------------------------------------------------- /SECURITY.md: -------------------------------------------------------------------------------- 1 | # Security Policy 2 | 3 | ## Supported Versions 4 | 5 | This product currently has no support and is experimental. That could change in future. 6 | 7 | 8 | ## Reporting a Vulnerability 9 | 10 | Please report any issues or vulerabilities with an [issue](https://github.com/bcgov/pubcode/issues). 11 | -------------------------------------------------------------------------------- /api/.dockerignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | -------------------------------------------------------------------------------- /api/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM node:22 AS build 2 | WORKDIR /app 3 | COPY *.json ./ 4 | RUN npm ci --omit=dev --ignore-scripts 5 | 6 | # Deployment container 7 | FROM gcr.io/distroless/nodejs22:nonroot 8 | ENV NODE_ENV=production 9 | 10 | # Copy over app 11 | WORKDIR /app 12 | COPY --from=build /app/node_modules ./node_modules 13 | COPY ./src ./src 14 | 15 | # Expose port - mostly a convention, for readability 16 | EXPOSE 3000 17 | 18 | USER 1001 19 | 20 | # Start up command 21 | HEALTHCHECK --interval=30s --timeout=3s CMD curl -f http://localhost/:3000 22 | CMD ["--max-old-space-size=100", "src/server"] 23 | -------------------------------------------------------------------------------- /api/README.md: -------------------------------------------------------------------------------- 1 | # Node Express App for Pubcode API 2 | 3 | This is a Node.js server application utilizing popular libraries and tools, including: 4 | 5 | 1. Express: Framework for building web applications 6 | 2. Morgan: Express middleware for logging 7 | 3. NoCache: Express middleware for prevent client-side caching 8 | 4. Helmet: Express middleware to secure an app using HTTP headers 9 | 5. CORS: Express middleware to allows cross-origin resource sharing 10 | 6. Body-Parser: Express middleware to parse incoming request for handling 11 | 7. Mongoose: MongoDB object modeling tool 12 | 8. Logger: Logs messages to the console 13 | 9. Express-Rate-Limit: Express middleware for rate limiting by IP 14 | 10. Dotenv: Zero-dependency module to loads environment variables (.env) 15 | 16 | #### Sample .env 17 | 18 | ``` 19 | DB_HOST= 20 | DB_PORT= 21 | DB_NAME= 22 | DB_USER= 23 | DB_PWD= 24 | API_KEY= 25 | ``` 26 | 27 | ```mermaid 28 | graph LR 29 | A[Client] --> B[Node Express App] 30 | B --> C[pubcode Router] 31 | C --> D[validate API Key Middleware] 32 | D -->|X-API-KEY valid| E[bulkLoad Service] 33 | D -->|X-API-KEY invalid| F[401 Unauthorized response] 34 | E -->|Success| G[200 Success response] 35 | E -->|Failure| H[500 Internal Server Error response with error log] 36 | C --> I[readAll Service] 37 | I -->|Success| J[200 Success response with data] 38 | I -->|Failure| H 39 | C --> K[findById Service] 40 | K -->|Success| L[200 Success response with data] 41 | K -->|Failure| H 42 | C --> M[health Service] 43 | M -->|Success| N[200 Success response with count] 44 | M -->|Failure| H 45 | ``` 46 | -------------------------------------------------------------------------------- /api/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "pubcode-api", 3 | "version": "1.0.0", 4 | "main": "src/server.js", 5 | "dependencies": { 6 | "async-retry": "^1.3.3", 7 | "axios": "^1.7.9", 8 | "axios-oauth-client": "^2.2.0", 9 | "axios-token-interceptor": "^0.2.0", 10 | "body-parser": "^2.0.0", 11 | "cors": "^2.8.5", 12 | "cron": "^4.0.0", 13 | "dotenv": "^17.0.0", 14 | "express": "^5.0.0", 15 | "express-rate-limit": "^7.5.0", 16 | "helmet": "^8.0.0", 17 | "lodash": "^4.17.21", 18 | "mongoose": "^8.10.0", 19 | "morgan": "^1.10.0", 20 | "nocache": "^4.0.0", 21 | "prom-client": "^15.1.3", 22 | "winston": "^3.17.0" 23 | }, 24 | "devDependencies": { 25 | "nodemon": "^3.1.9" 26 | }, 27 | "scripts": { 28 | "start": "node src/server.js", 29 | "dev": "nodemon src/server.js" 30 | }, 31 | "author": "Public Code API", 32 | "license": "Apache-2.0", 33 | "description": "Public Code API", 34 | "repository": { 35 | "type": "git", 36 | "url": "https://github.com/bcgov/pubcode.git", 37 | "directory": "api", 38 | "branch": "main" 39 | }, 40 | "nodemonConfig": { 41 | "ignore": [ 42 | "node_modules", 43 | "public" 44 | ], 45 | "watch": [ 46 | "src" 47 | ] 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /api/src/app.js: -------------------------------------------------------------------------------- 1 | const express = require("express"); 2 | const morgan = require("morgan"); 3 | const nocache = require("nocache"); 4 | const helmet = require("helmet"); 5 | const cors = require("cors"); 6 | const app = express(); 7 | const apiRouter = express.Router(); 8 | const pubcodeRouter = require("./routes/pubcode-router"); 9 | const log = require("./logger"); 10 | const prom = require('prom-client'); 11 | const register = new prom.Registry(); 12 | prom.collectDefaultMetrics({ register }); 13 | 14 | const rateLimit = require('express-rate-limit'); 15 | const limiter = rateLimit({ 16 | windowMs: 60 * 1000, // 1 minute 17 | max: 100, // Limit each IP to 100 requests per `window` (here, per 1 minutes) 18 | standardHeaders: true, // Return rate limit info in the `RateLimit-*` headers 19 | legacyHeaders: false, // Disable the `X-RateLimit-*` headers 20 | }); 21 | const logStream = { 22 | write: (message) => { 23 | log.info(message); 24 | } 25 | }; 26 | app.set("trust proxy", 1); 27 | app.use(limiter); 28 | app.use(cors()); 29 | app.use(helmet({ 30 | contentSecurityPolicy: { 31 | directives: { 32 | defaultSrc: ["'self'"], 33 | scriptSrc: ["'self'"], 34 | styleSrc: ["'self'", "'unsafe-inline'"], 35 | imgSrc: ["'self'", "data:", "https:"], 36 | }, 37 | }, 38 | })); 39 | app.use(nocache()); 40 | app.get('/metrics', async (_req, res) => { 41 | const appMetrics = await register.metrics(); 42 | res.end(appMetrics); 43 | }); 44 | //tells the app to use json as means of transporting data 45 | app.use(express.json({ limit: "50mb" })); 46 | app.use(express.urlencoded({ extended: true, limit: "50mb" })); 47 | app.use(morgan("dev", { stream: logStream, 48 | skip: (req, _res) => req.url === '/health' || req.url === '/' 49 | })); 50 | app.get("/", (req, res, next) => { 51 | res.sendStatus(200);// generally for route verification. 52 | }); 53 | app.use(/(\/api)?/, apiRouter); 54 | apiRouter.use("/pub-code", pubcodeRouter); 55 | app.use((req, res, next) => { 56 | res.status(404).send( 57 | "

Not Found.

"); 58 | }); 59 | 60 | module.exports = app; 61 | -------------------------------------------------------------------------------- /api/src/db/database.js: -------------------------------------------------------------------------------- 1 | const mongoose = require("mongoose"); 2 | const logger = require("../logger"); 3 | const DB_HOST = process.env.DB_HOST || "localhost"; 4 | const DB_USER = process.env.DB_USER || "default"; 5 | const DB_PWD = process.env.DB_PWD || "default"; 6 | const DB_PORT = process.env.DB_PORT || 27017; 7 | const DB_NAME = process.env.DB_NAME || "pubcode"; 8 | const retry = require("async-retry"); 9 | const database = async () => { 10 | await retry(async () => { 11 | const connectionUri = `mongodb://${DB_USER}:${DB_PWD}@${DB_HOST}:${DB_PORT}/${DB_NAME}`; 12 | logger.info(`connecting to mongodb on url: ${connectionUri}`); 13 | await mongoose.connect(connectionUri, { 14 | authSource: "admin", 15 | maxPoolSize: 20, 16 | minPoolSize: 5, 17 | serverSelectionTimeoutMS: 5000, 18 | socketTimeoutMS: 60000 19 | }); 20 | mongoose.connection.on("disconnected", () => { 21 | logger.error("disconnected from mongodb"); 22 | }); 23 | mongoose.connection.on("reconnected", () => { 24 | logger.info("reconnected to mongodb"); 25 | }); 26 | }); 27 | 28 | }; 29 | module.exports = { database }; 30 | -------------------------------------------------------------------------------- /api/src/email/ches-service.js: -------------------------------------------------------------------------------- 1 | const ClientConnection = require("./connection"); 2 | 3 | const SERVICE = "CHES"; 4 | 5 | class ChesService { 6 | constructor({ tokenUrl, clientId, clientSecret, apiUrl }) { 7 | if (!tokenUrl || !clientId || !clientSecret || !apiUrl) { 8 | console.log("Invalid configuration.", { function: "constructor" }); 9 | throw new Error("ChesService is not configured. Check configuration."); 10 | } 11 | this.connection = new ClientConnection({ 12 | tokenUrl, 13 | clientId, 14 | clientSecret 15 | }); 16 | this.axios = this.connection.axios; 17 | this.apiUrl = apiUrl; 18 | } 19 | 20 | async health() { 21 | try { 22 | const { data, status } = await this.axios.get(`${this.apiUrl}/health`, { 23 | headers: { 24 | "Content-Type": "application/json" 25 | } 26 | }); 27 | return { data, status }; 28 | } catch (e) { 29 | console.error(SERVICE, e); 30 | } 31 | } 32 | 33 | async getStatus(params, query) { 34 | try { 35 | if (params?.msgId) { 36 | const { data, status } = await this.axios.get( 37 | `${this.apiUrl}/status/${params.msgId}`, 38 | { 39 | headers: { 40 | "Content-Type": "application/json" 41 | } 42 | } 43 | ); 44 | return { data, status }; 45 | } else { 46 | const { data, status } = await this.axios.get(`${this.apiUrl}/status`, { 47 | params: query, 48 | headers: { 49 | "Content-Type": "application/json" 50 | } 51 | }); 52 | return { data, status }; 53 | } 54 | } catch (e) { 55 | console.error(SERVICE, e); 56 | } 57 | } 58 | 59 | async cancel(params, query) { 60 | try { 61 | if (params?.msgId) { 62 | const { data, status } = await this.axios.delete( 63 | `${this.apiUrl}/cancel/${params.msgId}`, 64 | { 65 | headers: { 66 | "Content-Type": "application/json" 67 | } 68 | } 69 | ); 70 | return { data, status }; 71 | } else { 72 | const { data, status } = await this.axios.delete( 73 | `${this.apiUrl}/cancel`, 74 | { 75 | params: query, 76 | headers: { 77 | "Content-Type": "application/json" 78 | } 79 | } 80 | ); 81 | return { data, status }; 82 | } 83 | } catch (e) { 84 | console.error(SERVICE, e); 85 | } 86 | } 87 | 88 | async send(email) { 89 | try { 90 | const { data, status } = await this.axios.post( 91 | `${this.apiUrl}/email`, 92 | email, 93 | { 94 | headers: { 95 | "Content-Type": "application/json" 96 | }, 97 | maxContentLength: Infinity, 98 | maxBodyLength: Infinity 99 | } 100 | ); 101 | return { data, status }; 102 | } catch (e) { 103 | console.error(e); 104 | console.error(SERVICE, e?.config?.data?.errors); 105 | } 106 | } 107 | 108 | async merge(mergeData) { 109 | try { 110 | const { data, status } = await this.axios.post( 111 | `${this.apiUrl}/emailMerge`, 112 | mergeData, 113 | { 114 | headers: { 115 | "Content-Type": "application/json" 116 | }, 117 | maxContentLength: Infinity, 118 | maxBodyLength: Infinity 119 | } 120 | ); 121 | return { data, status }; 122 | } catch (e) { 123 | console.error(SERVICE, e); 124 | } 125 | } 126 | 127 | async mergePeview(mergeData) { 128 | try { 129 | const { data, status } = await this.axios.post( 130 | `${this.apiUrl}/emailMerge/preview`, 131 | mergeData, 132 | { 133 | headers: { 134 | "Content-Type": "application/json" 135 | }, 136 | maxContentLength: Infinity, 137 | maxBodyLength: Infinity 138 | } 139 | ); 140 | return { data, status }; 141 | } catch (e) { 142 | console.error(SERVICE, e); 143 | } 144 | } 145 | 146 | async promote(params, query) { 147 | try { 148 | if (params?.msgId) { 149 | const { data, status } = await this.axios.post( 150 | `${this.apiUrl}/promote/${params.msgId}`, 151 | { 152 | headers: { 153 | "Content-Type": "application/json" 154 | } 155 | } 156 | ); 157 | return { data, status }; 158 | } else { 159 | const { data, status } = await this.axios.post( 160 | `${this.apiUrl}/promote`, 161 | { 162 | params: query, 163 | headers: { 164 | "Content-Type": "application/json" 165 | } 166 | } 167 | ); 168 | return { data, status }; 169 | } 170 | } catch (e) { 171 | console.error(SERVICE, e); 172 | } 173 | } 174 | 175 | generateHtmlEmail(subjectLine, to, title, body) { 176 | 177 | const emailContents= ` 178 | 179 | 180 | ${title} 181 | 182 | 183 | ${body} 184 | 185 | `; 186 | return { 187 | bodyType: 'html', 188 | body: emailContents, 189 | delayTS: 0, 190 | encoding: 'utf-8', 191 | from: "no-reply-bcgovpubcode@gov.bc.ca", 192 | priority: 'normal', 193 | subject: subjectLine, 194 | to: to 195 | }; 196 | } 197 | } 198 | 199 | module.exports = ChesService; 200 | -------------------------------------------------------------------------------- /api/src/email/connection.js: -------------------------------------------------------------------------------- 1 | const axios = require('axios'); 2 | const oauth = require('axios-oauth-client'); 3 | const tokenProvider = require('axios-token-interceptor'); 4 | 5 | class ClientConnection { 6 | 7 | constructor({tokenUrl, clientId, clientSecret}) { 8 | 9 | if (!tokenUrl || !clientId || !clientSecret) { 10 | console.log('Invalid configuration.', {function: 'constructor'}); 11 | throw new Error('ClientConnection is not configured. Check configuration.'); 12 | } 13 | 14 | this.tokenUrl = tokenUrl; 15 | 16 | this.axios = axios.create(); 17 | this.clientCreds = oauth.clientCredentials(axios.create(), this.tokenUrl, clientId, clientSecret); 18 | this.axios.interceptors.request.use(tokenProvider({ 19 | getToken: async () => { 20 | const data = await this.clientCreds(''); 21 | return data.access_token; 22 | }, 23 | })); 24 | } 25 | } 26 | 27 | module.exports = ClientConnection; 28 | -------------------------------------------------------------------------------- /api/src/email/index.js: -------------------------------------------------------------------------------- 1 | const ChesService = require("./ches-service"); 2 | 3 | const emailService = new ChesService({ 4 | tokenUrl: process.env.CHES_TOKEN_URL, 5 | clientId: process.env.CHES_CLIENT_ID, 6 | clientSecret: process.env.CHES_CLIENT_SECRET, 7 | apiUrl: process.env.CHES_API_URL, 8 | }); 9 | module.exports = emailService; 10 | -------------------------------------------------------------------------------- /api/src/entities/pub-code-entity.js: -------------------------------------------------------------------------------- 1 | const mongoose = require("mongoose"); 2 | 3 | const Schema = mongoose.Schema; 4 | 5 | const pubcode = new Schema( 6 | { 7 | repo_name: { 8 | type: String, 9 | required: true 10 | }, 11 | product_information: { 12 | type: Object, 13 | required: true 14 | }, 15 | data_management_roles: { 16 | type: Object, 17 | required: true 18 | }, 19 | product_technology_information: { 20 | type: Object, 21 | required: true 22 | }, 23 | product_external_dependencies: { 24 | type: Object 25 | }, 26 | version: { 27 | type: Number, 28 | required: true 29 | }, 30 | github_info: { 31 | type: Object 32 | }, 33 | is_deleted: { 34 | type: Boolean, 35 | default: false 36 | } 37 | }, 38 | { collection: "pubcode" } 39 | ); 40 | 41 | module.exports = mongoose.model("pubcode", pubcode); 42 | -------------------------------------------------------------------------------- /api/src/logger.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | const { createLogger, format, transports } = require("winston"); 4 | const { omit } = require("lodash"); 5 | 6 | 7 | /** 8 | * Handles all the different log formats 9 | * https://github.com/winstonjs/winston/issues/1427#issuecomment-535297716 10 | * https://github.com/winstonjs/winston/issues/1427#issuecomment-583199496 11 | * @param {*} colors 12 | */ 13 | function getDomainWinstonLoggerFormat(colors = true) { 14 | const colorize = colors ? format.colorize() : null; 15 | const loggingFormats = [ 16 | format.timestamp({ 17 | format: "YYYY-MM-DD HH:mm:ss.SSS" 18 | }), 19 | format.errors({ stack: true }), 20 | colorize, 21 | format.printf((info) => { 22 | const stackTrace = info.stack ? `\n${info.stack}` : ""; 23 | 24 | // handle single object 25 | if (!info.message) { 26 | const obj = omit(info, ["level", "timestamp", Symbol.for("level")]); 27 | return `${info.timestamp} - ${info.level}: ${obj}${stackTrace}`; 28 | } 29 | const splatArgs = info[Symbol.for("splat")] || []; 30 | const rest = splatArgs.join(" "); 31 | return `${info.timestamp} - ${info.level}: ${info.message} ${rest}${stackTrace}`; 32 | }) 33 | ].filter(Boolean); 34 | return format.combine(...loggingFormats); 35 | } 36 | 37 | 38 | const logger = createLogger({ 39 | level: process.env.LOG_LEVEL || "silly", 40 | format: getDomainWinstonLoggerFormat(true), 41 | transports: [ 42 | new transports.Console() 43 | ] 44 | }); 45 | 46 | module.exports = logger; 47 | -------------------------------------------------------------------------------- /api/src/routes/pubcode-router.js: -------------------------------------------------------------------------------- 1 | const express = require("express"); 2 | const router = express.Router(); 3 | const { bulkLoad, readAll, findById, health, softDeleteRepo } = require("../services/pub-code-service"); 4 | 5 | router.get("/ip/trace", (request, response) => response.send(request.ip)); 6 | router.get("/health", health); 7 | router.post("/bulk-load", (req, res, next) => { 8 | if (req.header("X-API-KEY") && req.header("X-API-KEY") === process.env.API_KEY) { 9 | next(); 10 | } else { 11 | res.status(401).json({ message: "Unauthorized" }); 12 | } 13 | }, bulkLoad); 14 | /** 15 | * This method allows for patching the objects with soft 16 | */ 17 | router.delete("/:repo_name", (req, res, next) => { 18 | if (req.header("X-API-KEY") && req.header("X-API-KEY") === process.env.API_KEY) { 19 | next(); 20 | } else { 21 | res.status(401).json({ message: "Unauthorized" }); 22 | } 23 | }, softDeleteRepo); 24 | router.get("/", readAll); 25 | 26 | module.exports = router; 27 | -------------------------------------------------------------------------------- /api/src/schedulers/refresh-cache.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | const { CronJob } = require("cron"); 3 | const log = require("../logger"); 4 | const cacheService = require("../services/cache-service"); 5 | 6 | try { 7 | // reload the cache every 5 minutes 8 | const reloadCache = new CronJob( 9 | "0 0/5 * * * *", 10 | async () => { 11 | log.debug("Starting reload cache"); 12 | try { 13 | await cacheService.loadAllPubCodes(); 14 | log.debug("reload cache completed"); 15 | } catch (e) { 16 | log.error(e); 17 | } 18 | }, 19 | null, 20 | false, 21 | 'America/Vancouver' 22 | ); 23 | reloadCache.start(); 24 | } catch (e) { 25 | log.error(e); 26 | } 27 | -------------------------------------------------------------------------------- /api/src/server.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | const http = require("http"); 3 | const log = require("./logger"); 4 | const dotenv = require("dotenv"); 5 | dotenv.config(); 6 | const app = require("./app"); 7 | const { database } = require("./db/database"); 8 | const port = normalizePort(process.env.PORT || 3000); 9 | const cacheService = require("./services/cache-service"); 10 | app.set("port", port); 11 | const server = http.createServer(app); 12 | database().then(() => { 13 | log.info("mongoose initialized"); 14 | server.listen(port); 15 | server.on("error", onError); 16 | server.on("listening", onListening); 17 | cacheService.loadAllPubCodes().then(() => { 18 | log.info("pub codes loaded to cache"); 19 | }).catch((error) => { 20 | log.error(error); 21 | }); 22 | require("./schedulers/refresh-cache"); 23 | }).catch((error) => { 24 | log.error(error); 25 | process.exit(1); 26 | }); 27 | 28 | /** 29 | * Normalize a port into a number, string, or false. 30 | */ 31 | function normalizePort(val) { 32 | const portNumber = parseInt(val, 10); 33 | 34 | if (isNaN(portNumber)) { 35 | // named pipe 36 | return val; 37 | } 38 | 39 | if (portNumber >= 0) { 40 | // port number 41 | return portNumber; 42 | } 43 | 44 | return false; 45 | } 46 | 47 | /** 48 | * Event listener for HTTP server "error" event. 49 | */ 50 | function onError(error) { 51 | if (error.syscall !== "listen") { 52 | throw error; 53 | } 54 | 55 | const bind = typeof port === "string" ? 56 | "Pipe " + port : 57 | "Port " + port; 58 | 59 | // handle specific listen errors with friendly messages 60 | switch (error.code) { 61 | case "EACCES": 62 | log.error(bind + " requires elevated privileges"); 63 | break; 64 | case "EADDRINUSE": 65 | log.error(bind + " is already in use"); 66 | break; 67 | default: 68 | throw error; 69 | } 70 | } 71 | 72 | /** 73 | * Event listener for HTTP server "listening" event. 74 | */ 75 | function onListening() { 76 | const addr = server.address(); 77 | const bind = typeof addr === "string" ? 78 | "pipe " + addr : 79 | "port " + addr.port; 80 | log.info("Listening on " + bind); 81 | } 82 | 83 | process.on("SIGINT", () => { 84 | server.close(() => { 85 | log.info("process terminated"); 86 | process.exit(0); 87 | }); 88 | }); 89 | process.on("SIGTERM", () => { 90 | server.close(() => { 91 | log.info("process terminated"); 92 | process.exit(0); 93 | }); 94 | }); 95 | -------------------------------------------------------------------------------- /api/src/services/cache-service.js: -------------------------------------------------------------------------------- 1 | const pubcodeEntity = require("../entities/pub-code-entity"); 2 | let PUB_CODES = []; 3 | const cacheService = { 4 | 5 | async loadAllPubCodes() { 6 | const data = await pubcodeEntity.find({}); 7 | const newPubCodes = []; 8 | for (const individualData of data) { 9 | if(!individualData.is_deleted) { 10 | const normalizedResult = { 11 | repo_name: individualData.repo_name, 12 | ...individualData.product_information, 13 | ...individualData.product_technology_information, 14 | ...individualData.product_external_dependencies, 15 | ...individualData.data_management_roles, 16 | ...individualData.github_info, 17 | }; 18 | newPubCodes.push(normalizedResult); 19 | } 20 | } 21 | // clear the existing cache 22 | if (PUB_CODES && PUB_CODES.length > 0) { 23 | PUB_CODES = []; 24 | } 25 | PUB_CODES = newPubCodes; 26 | return PUB_CODES; 27 | }, 28 | getAllPubCodes() { 29 | return PUB_CODES; 30 | } 31 | }; 32 | module.exports = cacheService; 33 | -------------------------------------------------------------------------------- /api/src/services/pub-code-service.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | const logger = require("../logger"); 3 | const pubcodeEntity = require("../entities/pub-code-entity"); 4 | const cacheService = require("../services/cache-service"); 5 | const emailService = require("../email"); 6 | const EMAIL_RECIPIENTS = process.env.EMAIL_RECIPIENTS?.split(","); 7 | 8 | /** 9 | * The below method will be called from the router after validating the x-api-key in the router layer. 10 | * it will take the request body and parse it into a json object array. it is expected that the request body is a json array of objects. 11 | * the json array of objects will be validated against the schema and then inserted into the database. 12 | * it will delete and add all these new in a single transaction into mongoDB. 13 | */ 14 | async function insertOrUpdate(payload, notInsertedArray) { 15 | let repoWithErrors = []; 16 | for (const pubcode of payload) { 17 | const entity = new pubcodeEntity({ ...pubcode }); 18 | const errors = entity.validateSync(); 19 | if (errors) { 20 | console.info(`insertOrUpdate: ${pubcode.repo_name} is not valid`); 21 | logger.error("insertOrUpdate: ", errors); 22 | repoWithErrors.push(pubcode.repo_name); 23 | continue; 24 | } 25 | const foundEntity = await pubcodeEntity.findOneAndReplace({ repo_name: pubcode.repo_name }, pubcode).exec(); 26 | if (!foundEntity) { 27 | notInsertedArray.push(pubcode); 28 | } 29 | } 30 | if (notInsertedArray.length > 0) { 31 | await pubcodeEntity.insertMany(notInsertedArray); 32 | } 33 | if (repoWithErrors?.length > 0 && EMAIL_RECIPIENTS) { 34 | try { 35 | const email = emailService.generateHtmlEmail("Error During Bulk Load of Pub Codes Some Repo Contains are not valid.", EMAIL_RECIPIENTS, "Error During Bulk Load of Pub Codes Some Repo Contains are not valid.", `

${repoWithErrors.join(",")}

`); 36 | await emailService.send(email); 37 | } catch (e) { 38 | logger.error("bulkLoad: ", e); 39 | } 40 | } 41 | } 42 | 43 | async function sendEmailForError(error) { 44 | if (EMAIL_RECIPIENTS) { 45 | try { 46 | const email = emailService.generateHtmlEmail("Error During Bulk Load of Pub Codes Saving to Database Failed", EMAIL_RECIPIENTS, "Error During Bulk Load of Pub Codes Saving to Database Failed.", `

${error.message}

`); 47 | await emailService.send(email); 48 | } catch (e) { 49 | logger.error("bulkLoad: ", e); 50 | } 51 | } 52 | } 53 | 54 | const bulkLoad = async (req, res) => { 55 | try { 56 | const notInsertedArray = []; 57 | const payload = req.body; 58 | if (payload && Array.isArray(payload) && payload.length > 0) { 59 | insertOrUpdate(payload, notInsertedArray).catch(async (error) => { 60 | await sendEmailForError(error); 61 | logger.error("bulkLoad: ", error); 62 | }); 63 | res.status(200).json({}); 64 | } else { 65 | res.status(400).json({ message: "Invalid request body" }); 66 | } 67 | 68 | } catch (error) { 69 | logger.error("bulkLoad: ", error); 70 | if (EMAIL_RECIPIENTS) { 71 | try { 72 | const email = emailService.generateHtmlEmail("Error During Bulk Load of Pub Codes", EMAIL_RECIPIENTS, "Error During Bulk Load of Pub Codes", error.message); 73 | await emailService.send(email); 74 | } catch (e) { 75 | logger.error("bulkLoad: ", e); 76 | } 77 | } 78 | 79 | res.status(500).json(error); 80 | } 81 | }; 82 | const readAll = async (req, res) => { 83 | try { 84 | res.status(200).json(cacheService.getAllPubCodes()); 85 | } catch (error) { 86 | logger.error("readAll: ", error); 87 | if (EMAIL_RECIPIENTS) { 88 | try { 89 | const email = emailService.generateHtmlEmail("Error During Read ALl of Pub Codes", EMAIL_RECIPIENTS, "Error During Read ALl of Pub Codes", error.message); 90 | await emailService.send(email); 91 | } catch (e) { 92 | logger.error("bulkLoad: ", e); 93 | } 94 | } 95 | res.status(500).json(error); 96 | } 97 | }; 98 | 99 | const findById = async (req, res) => { 100 | try { 101 | const { id } = req.params; 102 | const result = await pubcodeEntity.findById(id).exec(); 103 | res.status(200).json(result); 104 | } catch (error) { 105 | logger.error("findById: ", error); 106 | res.status(500).json(error); 107 | } 108 | }; 109 | const health = async (req, res) => { 110 | try { 111 | const result = await pubcodeEntity.countDocuments(); 112 | res.status(200).json(result); 113 | } catch (error) { 114 | logger.error("health: ", error); 115 | res.status(500).json(error); 116 | } 117 | }; 118 | const softDeleteRepo = async (req, res) => { 119 | try { 120 | let pubcodeEntityFromDB = await pubcodeEntity.findOne({ repo_name: req.params.repo_name }).exec(); 121 | if (!pubcodeEntityFromDB) { 122 | res.status(404).json({ message: "Repo Not Found" }); 123 | } else { 124 | await pubcodeEntity.updateOne({ _id: pubcodeEntityFromDB["_id"] }, { is_deleted: true }).exec(); 125 | pubcodeEntityFromDB = await pubcodeEntity.findOne({ repo_name: req.params.repo_name }).exec(); 126 | console.info(pubcodeEntityFromDB); 127 | res.status(200).json({ message: "Repo Marked as soft deleted." }); 128 | } 129 | } catch (e) { 130 | console.error(e); 131 | } 132 | 133 | }; 134 | module.exports = { 135 | bulkLoad, 136 | readAll, 137 | findById, 138 | health, 139 | softDeleteRepo 140 | }; 141 | -------------------------------------------------------------------------------- /bcgovpubcode.yml: -------------------------------------------------------------------------------- 1 | version: 1 2 | data_management_roles: 3 | data_custodian: Om 4 | data_steward: Om 5 | product_owner: Jeff Card 6 | product_information: 7 | ministry: 8 | - Water, Land and Resource Stewardship 9 | product_acronym: PUBCODE 10 | product_description: Public Code Asset Management Schema 11 | product_name: Public Code 12 | product_status: active 13 | product_urls: 14 | - https://pubcode.apps.silver.devops.gov.bc.ca 15 | - https://pubcode-api.apps.silver.devops.gov.bc.ca/api/pub-code 16 | program_area: Arch & Bus Strategy 17 | product_technology_information: 18 | backend_frameworks: 19 | - name: Express 20 | version: 4.x 21 | backend_languages_version: 22 | - name: JavaScript 23 | version: es2022 24 | ci_cd_tools: 25 | - GitHub-Actions 26 | data_storage_platforms: 27 | - Mongodb 28 | frontend_frameworks: 29 | - name: React 30 | version: 18.x 31 | frontend_languages: 32 | - name: JavaScript 33 | version: es2022 34 | hosting_platforms: 35 | - Private-Cloud-Openshift 36 | -------------------------------------------------------------------------------- /charts/pubcode/.helmignore: -------------------------------------------------------------------------------- 1 | # Patterns to ignore when building packages. 2 | # This supports shell glob matching, relative path matching, and 3 | # negation (prefixed with !). Only one pattern per line. 4 | .DS_Store 5 | # Common VCS dirs 6 | .git/ 7 | .gitignore 8 | .bzr/ 9 | .bzrignore 10 | .hg/ 11 | .hgignore 12 | .svn/ 13 | # Common backup files 14 | *.swp 15 | *.bak 16 | *.tmp 17 | *.orig 18 | *~ 19 | # Various IDEs 20 | .project 21 | .idea/ 22 | *.tmproj 23 | .vscode/ 24 | -------------------------------------------------------------------------------- /charts/pubcode/Chart.lock: -------------------------------------------------------------------------------- 1 | dependencies: 2 | - name: component 3 | repository: https://bcgov.github.io/helm-service/ 4 | version: 0.3.3 5 | - name: component 6 | repository: https://bcgov.github.io/helm-service/ 7 | version: 0.3.3 8 | - name: component 9 | repository: https://bcgov.github.io/helm-service/ 10 | version: 0.3.3 11 | digest: sha256:d1cd17e5ba97860d19a838a3b8dd85f9601c6053a9791d339c05cb06d64a7087 12 | generated: "2025-03-15T01:56:59.766695711Z" 13 | -------------------------------------------------------------------------------- /charts/pubcode/Chart.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v2 2 | name: pubcode 3 | description: A Helm chart for Kubernetes 4 | 5 | # A chart can be either an 'application' or a 'library' chart. 6 | # 7 | # Application charts are a collection of templates that can be packaged into versioned archives 8 | # to be deployed. 9 | # 10 | # Library charts provide useful utilities or functions for the chart developer. They're included as 11 | # a dependency of application charts to inject those utilities and functions into the rendering 12 | # pipeline. Library charts do not define any templates and therefore cannot be deployed. 13 | type: application 14 | 15 | # This is the chart version. This version number should be incremented each time you make changes 16 | # to the chart and its templates, including the app version. 17 | # Versions are expected to follow Semantic Versioning (https://semver.org/) 18 | version: 0.1.0 19 | 20 | # This is the version number of the application being deployed. This version number should be 21 | # incremented each time you make changes to the application. Versions are not expected to 22 | # follow Semantic Versioning. They should reflect the version the application is using. 23 | # It is recommended to use it with quotes. 24 | appVersion: "0.1.0" 25 | 26 | dependencies: 27 | 28 | - name: component 29 | condition: api.enabled 30 | version: 0.3.3 31 | repository: https://bcgov.github.io/helm-service/ 32 | alias: api 33 | 34 | - name: component 35 | condition: frontend.enabled 36 | version: 0.3.3 37 | repository: https://bcgov.github.io/helm-service/ 38 | alias: frontend 39 | 40 | - name: component 41 | condition: database.enabled 42 | version: 0.3.3 43 | repository: https://bcgov.github.io/helm-service/ 44 | alias: database 45 | -------------------------------------------------------------------------------- /charts/pubcode/templates/_helpers.tpl: -------------------------------------------------------------------------------- 1 | {{/* 2 | Expand the name of the chart. 3 | */}} 4 | {{- define "name" -}} 5 | {{- default .Chart.Name .Values.nameOverride | trunc 63 | trimSuffix "-" }} 6 | {{- end }} 7 | 8 | {{/* 9 | Create a default fully qualified app name. 10 | We truncate at 63 chars because some Kubernetes name fields are limited to this (by the DNS naming spec). 11 | If release name contains chart name it will be used as a full name. 12 | */}} 13 | {{- define "fullname" -}} 14 | {{- if .Values.fullnameOverride }} 15 | {{- .Values.fullnameOverride | trunc 63 | trimSuffix "-" }} 16 | {{- else }} 17 | {{- printf "%s" .Release.Name | trunc 63 | trimSuffix "-" }} 18 | {{- end }} 19 | {{- end }} 20 | 21 | {{/* 22 | Create chart name and version as used by the chart label. 23 | */}} 24 | {{- define "name.chart" -}} 25 | {{- printf "%s-%s" .Chart.Name .Chart.Version | replace "+" "_" | trunc 63 | trimSuffix "-" }} 26 | {{- end }} 27 | 28 | {{/* 29 | Common labels 30 | */}} 31 | {{- define "labels" -}} 32 | helm.sh/chart: {{ include "name.chart" . }} 33 | {{ include "selectorLabels" . }} 34 | {{- if .Chart.AppVersion }} 35 | app.kubernetes.io/version: {{ .Chart.AppVersion | quote }} 36 | {{- end }} 37 | app.kubernetes.io/managed-by: {{ .Release.Service }} 38 | {{- end }} 39 | 40 | {{/* 41 | Selector labels 42 | */}} 43 | {{- define "selectorLabels" -}} 44 | app.kubernetes.io/name: {{ include "fullname" . }} 45 | app.kubernetes.io/instance: {{ .Release.Name }} 46 | {{- end }} 47 | 48 | 49 | -------------------------------------------------------------------------------- /charts/pubcode/templates/knp.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | apiVersion: networking.k8s.io/v1 3 | kind: NetworkPolicy 4 | metadata: 5 | name: {{ .Release.Name }}-openshift-ingress 6 | labels: {{- include "selectorLabels" . | nindent 4 }} 7 | spec: 8 | podSelector: {} 9 | ingress: 10 | - from: 11 | - namespaceSelector: 12 | matchLabels: 13 | network.openshift.io/policy-group: ingress 14 | policyTypes: 15 | - Ingress 16 | --- 17 | apiVersion: networking.k8s.io/v1 18 | kind: NetworkPolicy 19 | metadata: 20 | name: {{ .Release.Name }}-allow-same-namespace 21 | labels: {{- include "selectorLabels" . | nindent 4 }} 22 | spec: 23 | podSelector: {} 24 | ingress: 25 | - from: 26 | - podSelector: {} 27 | policyTypes: 28 | - Ingress 29 | 30 | -------------------------------------------------------------------------------- /charts/pubcode/templates/secret.yaml: -------------------------------------------------------------------------------- 1 | {{- if and .Values.global.secrets .Values.global.secrets.enabled}} 2 | # retrieve the secret data using lookup function and when not exists, return an empty dictionary / map as result 3 | {{- $secretObj := (lookup "v1" "Secret" .Release.Namespace .Release.Name ) | default dict }} 4 | {{- $secretData := (get $secretObj "data") | default dict }} 5 | # set below to existing secret data or generate a random one when not exists 6 | {{- $apiKey := (get $secretData "API_KEY") | default (randAlphaNum 32 | b64enc) }} 7 | --- 8 | apiVersion: v1 9 | kind: Secret 10 | metadata: 11 | name: {{ .Release.Name }} 12 | labels: {{- include "selectorLabels" . | nindent 4 }} 13 | data: 14 | DB_HOST: {{ tpl .Values.global.secrets.databaseHost $ | b64enc | quote }} 15 | DB_PWD: {{ tpl .Values.global.secrets.databaseAdminPassword $ | b64enc | quote }} 16 | DB_USER: {{ tpl .Values.global.secrets.databaseAdminUser $ | b64enc | quote }} 17 | DB_NAME: {{ tpl .Values.global.secrets.databaseName $ | b64enc | quote }} 18 | API_KEY: {{ $apiKey | quote }} 19 | EMAIL_RECIPIENTS: {{ tpl .Values.global.secrets.emailRecipients $ | b64enc | quote }} 20 | CHES_TOKEN_URL: {{ tpl .Values.global.secrets.chesTokenURL $ | b64enc | quote }} 21 | CHES_CLIENT_ID: {{ tpl .Values.global.secrets.chesClientID $ | b64enc | quote }} 22 | CHES_CLIENT_SECRET: {{ tpl .Values.global.secrets.chesClientSecret $ | b64enc | quote }} 23 | CHES_API_URL: {{ tpl .Values.global.secrets.chesAPIURL $ | b64enc | quote }} 24 | MONGO_INITDB_ROOT_USERNAME: {{ tpl .Values.global.secrets.databaseAdminUser $ | b64enc | quote }} 25 | MONGO_INITDB_ROOT_PASSWORD: {{ tpl .Values.global.secrets.databaseAdminPassword $ | b64enc | quote }} 26 | MONGODB_DATABASE: {{ tpl .Values.global.secrets.databaseName $ | b64enc | quote }} 27 | 28 | --- 29 | apiVersion: v1 30 | kind: Secret 31 | metadata: 32 | name: {{ .Release.Name }}-frontend 33 | labels: {{- include "selectorLabels" . | nindent 4 }} 34 | data: 35 | VITE_POWERBI_URL: {{ tpl .Values.global.secrets.powerBIURL $ | b64enc | quote }} 36 | {{- end }} 37 | -------------------------------------------------------------------------------- /charts/pubcode/values.yaml: -------------------------------------------------------------------------------- 1 | # This is a YAML-formatted file. 2 | # Declare variables to be passed into your templates. 3 | global: 4 | repository: ~ # the repository where the images are stored. 5 | registry: ghcr.io # the registry where the images are stored. override during runtime for other registry at global level or individual level. 6 | env: 7 | LOG_LEVEL: "info" 8 | VITE_SCHEMA_BRANCH: "main" 9 | secrets: 10 | enabled: true 11 | databaseHost: '{{ .Release.Name }}-database' 12 | databaseAdminPassword: default 13 | databaseAdminUser: admin 14 | databaseName: pubcode 15 | apiKey: default 16 | emailRecipients: default 17 | chesTokenURL: default 18 | chesClientID: default 19 | chesClientSecret: default 20 | chesAPIURL: default 21 | powerBIURL: ~ 22 | domain: "apps.silver.devops.gov.bc.ca" # it is required, apps.silver.devops.gov.bc.ca for silver cluster 23 | openshiftImageRegistry: "image-registry.openshift-image-registry.svc:5000" 24 | imagestreams: 25 | enabled: false 26 | 27 | api: 28 | enabled: true 29 | deployment: # can be either a statefulSet or a deployment not both 30 | enabled: true 31 | 32 | containers: 33 | - name: api 34 | registry: '{{ .Values.global.registry }}' 35 | repository: '{{ .Values.global.repository }}' # example, it includes registry and repository 36 | image: api # the exact component name, be it backend, api-1 etc... 37 | tag: prod # the tag of the image, it can be latest, 1.0.0 etc..., or the sha256 hash 38 | envFrom: 39 | secretRef: 40 | name: '{{ .Release.Name }}' 41 | ports: 42 | - name: http 43 | containerPort: 3000 44 | protocol: TCP 45 | resources: # this is optional 46 | requests: 47 | cpu: 50m 48 | memory: 100Mi 49 | readinessProbe: 50 | httpGet: 51 | path: /api/pub-code/health 52 | port: 3000 53 | scheme: HTTP 54 | initialDelaySeconds: 5 55 | periodSeconds: 2 56 | timeoutSeconds: 2 57 | successThreshold: 1 58 | failureThreshold: 30 59 | livenessProbe: 60 | successThreshold: 1 61 | failureThreshold: 3 62 | httpGet: 63 | path: /api/pub-code/health 64 | port: 3000 65 | scheme: HTTP 66 | initialDelaySeconds: 15 67 | periodSeconds: 30 68 | timeoutSeconds: 5 69 | 70 | autoscaling: 71 | enabled: true 72 | minReplicas: 3 73 | maxReplicas: 5 74 | targetCPUUtilizationPercentage: 80 # this percentage from request cpu 75 | vault: 76 | enabled: false 77 | service: 78 | enabled: true 79 | type: ClusterIP 80 | ports: 81 | - name: http 82 | port: 80 83 | targetPort: 3000 # the container port where the application is listening on 84 | protocol: TCP 85 | nodeSelector: { } 86 | tolerations: [ ] 87 | affinity: { } 88 | route: 89 | enabled: true 90 | host: "{{ .Release.Name }}-api.{{ .Values.global.domain }}" 91 | targetPort: http # look at line#164 refer to the name. 92 | podAnnotations: |- 93 | prometheus.io/scrape: "true" 94 | prometheus.io/port: "3000" 95 | prometheus.io/path: "/metrics" 96 | 97 | frontend: 98 | enabled: true 99 | deployment: # can be either a statefulSet or a deployment not both 100 | enabled: true 101 | containers: 102 | - name: frontend 103 | registry: '{{ .Values.global.registry }}' # example, it includes registry 104 | repository: '{{ .Values.global.repository }}' # example, it includes repository 105 | image: frontend # the exact component name, be it backend, api-1 etc... 106 | tag: prod # the tag of the image, it can be latest, 1.0.0 etc..., or the sha256 hash 107 | securityContext: 108 | capabilities: 109 | add: [ "NET_BIND_SERVICE" ] 110 | envFrom: 111 | secretRef: 112 | name: '{{ .Release.Name }}-frontend' 113 | env: 114 | fromValues: 115 | - name: VITE_SCHEMA_BRANCH 116 | value: '{{ .Values.global.env.VITE_SCHEMA_BRANCH }}' 117 | - name: LOG_LEVEL 118 | value: '{{ .Values.global.env.LOG_LEVEL }}' 119 | - name: BACKEND_URL 120 | value: "http://{{ .Release.Name }}-api" 121 | ports: 122 | - name: http 123 | containerPort: 3000 124 | protocol: TCP 125 | - name: http2 126 | containerPort: 3001 127 | protocol: TCP 128 | resources: # this is optional 129 | requests: 130 | cpu: 50m 131 | memory: 50Mi 132 | readinessProbe: 133 | httpGet: 134 | path: /health 135 | port: 3001 136 | scheme: HTTP 137 | initialDelaySeconds: 5 138 | periodSeconds: 2 139 | timeoutSeconds: 2 140 | successThreshold: 1 141 | failureThreshold: 30 142 | livenessProbe: 143 | successThreshold: 1 144 | failureThreshold: 3 145 | httpGet: 146 | path: /health 147 | port: 3001 148 | scheme: HTTP 149 | initialDelaySeconds: 15 150 | periodSeconds: 30 151 | timeoutSeconds: 5 152 | autoscaling: 153 | enabled: true 154 | minReplicas: 3 155 | maxReplicas: 5 156 | targetCPUUtilizationPercentage: 80 # this percentage from request cpu 157 | service: 158 | enabled: true 159 | type: ClusterIP 160 | ports: 161 | - name: http 162 | port: 80 163 | targetPort: 3000 # the container port where the application is listening on 164 | protocol: TCP 165 | ingress: 166 | className: openshift-default 167 | annotations: 168 | haproxy.router.openshift.io/balance: "roundrobin" 169 | route.openshift.io/termination: "edge" 170 | haproxy.router.openshift.io/rate-limit-connections: "true" 171 | haproxy.router.openshift.io/rate-limit-connections.concurrent-tcp: "20" 172 | haproxy.router.openshift.io/rate-limit-connections.rate-http: "50" 173 | haproxy.router.openshift.io/rate-limit-connections.rate-tcp: "100" 174 | haproxy.router.openshift.io/disable_cookies: "true" 175 | enabled: true 176 | hosts: 177 | - host: "{{ .Release.Name }}.{{ .Values.global.domain }}" 178 | paths: 179 | - path: / 180 | pathType: ImplementationSpecific 181 | 182 | podAnnotations: |- 183 | prometheus.io/scrape: "true" 184 | prometheus.io/port: "3002" 185 | prometheus.io/path: "/metrics" 186 | 187 | database: 188 | enabled: true 189 | deployment: # can be either a statefulSet or a deployment not both 190 | enabled: true 191 | deploymentStrategy: 192 | type: Recreate 193 | volumes: 194 | - name: '{{ .Release.Name }}-database' 195 | persistentVolumeClaim: 196 | claimName: '{{ .Release.Name }}-database' 197 | - name: '{{ include "component.fullname" . }}-config' 198 | configMap: 199 | name: '{{ include "component.fullname" . }}' 200 | initContainers: 201 | - name: database-init 202 | registry: 'ghcr.io' # example, it includes registry 203 | repository: 'bcgov/nr-containers' # example, it includes repository 204 | image: mongo # the exact component name, be it backend, api-1 etc... 205 | tag: 7.0.20 # the tag of the image, it can be latest, 1.0.0 etc..., or the sha256 hash 206 | command: 207 | - "sh" 208 | - "-c" 209 | - "mkdir -p /data/db" 210 | resources: # this is optional 211 | requests: 212 | cpu: 50m 213 | memory: 150Mi 214 | volumeMounts: 215 | - name: '{{ .Release.Name }}-database' 216 | mountPath: /data/db 217 | containers: 218 | - name: database 219 | registry: 'ghcr.io' # example, it includes registry 220 | repository: 'bcgov/nr-containers' # example, it includes repository 221 | image: mongo # the exact component name, be it backend, api-1 etc... 222 | tag: 7.0.20 # the tag of the image, it can be latest, 1.0.0 etc..., or the sha256 hash 223 | envFrom: 224 | secretRef: 225 | name: '{{ .Release.Name }}' 226 | ports: 227 | - name: http 228 | containerPort: 27017 229 | protocol: TCP 230 | resources: # this is optional 231 | requests: 232 | cpu: 50m 233 | memory: 150Mi 234 | readinessProbe: 235 | tcpSocket: 236 | port: 27017 237 | initialDelaySeconds: 10 238 | periodSeconds: 15 239 | timeoutSeconds: 5 240 | failureThreshold: 30 241 | livenessProbe: 242 | failureThreshold: 20 243 | initialDelaySeconds: 60 244 | periodSeconds: 15 245 | tcpSocket: 246 | port: 27017 247 | timeoutSeconds: 5 248 | volumeMounts: 249 | - name: '{{ .Release.Name }}-database' 250 | mountPath: /data/db 251 | autoscaling: 252 | enabled: false 253 | minReplicas: 1 254 | maxReplicas: 1 255 | targetCPUUtilizationPercentage: 80 # this percentage from request cpu 256 | service: 257 | enabled: true 258 | type: ClusterIP 259 | ports: 260 | - name: http 261 | port: 27017 262 | targetPort: 27017 # the container port where the application is listening on 263 | protocol: TCP 264 | pvc: 265 | enabled: true 266 | size: 750Mi 267 | storageClassName: netapp-file-standard 268 | accessModes: ReadWriteMany 269 | -------------------------------------------------------------------------------- /common/graphics/deploymentUpdate.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bcgov/pubcode/d20f86f24d2ee13cfc726d2412227eab6cb563f3/common/graphics/deploymentUpdate.png -------------------------------------------------------------------------------- /common/graphics/main-merge.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bcgov/pubcode/d20f86f24d2ee13cfc726d2412227eab6cb563f3/common/graphics/main-merge.png -------------------------------------------------------------------------------- /common/graphics/merge-main.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bcgov/pubcode/d20f86f24d2ee13cfc726d2412227eab6cb563f3/common/graphics/merge-main.png -------------------------------------------------------------------------------- /common/graphics/mergeNotification.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bcgov/pubcode/d20f86f24d2ee13cfc726d2412227eab6cb563f3/common/graphics/mergeNotification.png -------------------------------------------------------------------------------- /common/graphics/packages.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bcgov/pubcode/d20f86f24d2ee13cfc726d2412227eab6cb563f3/common/graphics/packages.png -------------------------------------------------------------------------------- /common/graphics/pr-cleanup.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bcgov/pubcode/d20f86f24d2ee13cfc726d2412227eab6cb563f3/common/graphics/pr-cleanup.png -------------------------------------------------------------------------------- /common/graphics/pr-close.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bcgov/pubcode/d20f86f24d2ee13cfc726d2412227eab6cb563f3/common/graphics/pr-close.png -------------------------------------------------------------------------------- /common/graphics/pr-open.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bcgov/pubcode/d20f86f24d2ee13cfc726d2412227eab6cb563f3/common/graphics/pr-open.png -------------------------------------------------------------------------------- /common/graphics/template.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bcgov/pubcode/d20f86f24d2ee13cfc726d2412227eab6cb563f3/common/graphics/template.png -------------------------------------------------------------------------------- /common/graphics/unit-tests.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bcgov/pubcode/d20f86f24d2ee13cfc726d2412227eab6cb563f3/common/graphics/unit-tests.png -------------------------------------------------------------------------------- /crawler/README.md: -------------------------------------------------------------------------------- 1 | ```mermaid 2 | sequenceDiagram 3 | participant Crawler 4 | participant API 5 | participant GH Repository 6 | participant GH API 7 | participant Dotenv 8 | 9 | Crawler->>Dotenv: Load Configuration 10 | Dotenv->>Crawler: Configuration Loaded 11 | Crawler->>API: Authenticate API using GIT_TOKEN 12 | API->>Crawler: API Authenticated 13 | Crawler->>GH API: Fetch 100 Repositories 14 | GH API->>Crawler: Repository Info 15 | loop until all Repositories Fetched 16 | Crawler->>GH API: Repeat Fetch 100 Repositories 17 | GH API->>Crawler: Repository Info 18 | end 19 | Crawler->>GH Repository: Fetch bcgovpubcode.yml 20 | GH Repository->>Crawler: bcgovpubcode.yml 21 | Crawler->>API: Store bcgovpubcode.yml 22 | 23 | ``` 24 | -------------------------------------------------------------------------------- /crawler/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "pubcode-crawler", 3 | "private": true, 4 | "version": "0.0.0", 5 | "type": "module", 6 | "scripts": { 7 | "dev": "nodemon --watch src ./src/main.js" 8 | }, 9 | "dependencies": { 10 | "async-retry": "^1.3.3", 11 | "axios": "^1.6.8", 12 | "dotenv": "^17.0.0", 13 | "js-yaml": "^4.1.0" 14 | }, 15 | "devDependencies": { 16 | "nodemon": "^3.1.0" 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /crawler/src/main.js: -------------------------------------------------------------------------------- 1 | import axios from "axios"; 2 | import * as dotenv from "dotenv"; 3 | import jsYaml from "js-yaml"; 4 | 5 | dotenv.config(); 6 | const token = process.env.GIT_TOKEN; 7 | const repoWithDetailsArray = []; 8 | 9 | const API_KEY = process.env.API_KEY; 10 | const API_URL = process.env.API_URL; 11 | const REPO_NAMES = process.env.REPO_NAMES; //comma separated list of repo names within bcgov org 12 | const BASE_DELAY = process.env.BASE_DELAY ? parseInt(process.env.BASE_DELAY, 10) : 60000; // 1 minute base delay 13 | const MAX_DELAY = process.env.MAX_DELAY ? parseInt(process.env.MAX_DELAY, 10) : 600000; // 10 minutes maximum delay 14 | const JITTER_FACTOR = Math.random() * 0.3; // Random jitter between 0-30% 15 | /** 16 | * Fetches the bcgovpubcode yaml for the specified repo and branch 17 | * @param repoName 18 | * @param branchName 19 | * @returns {Promise>} 20 | */ 21 | async function getYamlFromRepo(repoName, branchName) { 22 | let yamlResponse; 23 | try { 24 | yamlResponse = await axios.get( 25 | `https://raw.githubusercontent.com/bcgov/${repoName}/${branchName}/bcgovpubcode.yml` 26 | ); 27 | } catch (e) { 28 | if (e.response?.status === 404) { 29 | yamlResponse = await axios.get( 30 | `https://raw.githubusercontent.com/bcgov/${repoName}/${branchName}/bcgovpubcode.yaml` 31 | ); 32 | } 33 | } 34 | return yamlResponse; 35 | } 36 | 37 | /** 38 | * convert yaml response to a JSON object and add additional GitHub attributes 39 | * @param yamlResponse 40 | * @param repoWithDetails 41 | * @returns {JSON object of yaml with additional github attributes} 42 | */ 43 | 44 | function processYamlFromHttpResponse(yamlResponse, repoWithDetails) { 45 | const yaml = yamlResponse.data; 46 | const yamlJson = jsYaml.load(yaml); 47 | yamlJson.repo_name = repoWithDetails.name; 48 | yamlJson.github_info = { 49 | last_updated: repoWithDetails.lastUpdated?.substring(0, 10), 50 | license: repoWithDetails.license, 51 | watchers: repoWithDetails.watchers, 52 | stars: repoWithDetails.stars, 53 | default_branch: repoWithDetails.defaultBranch, 54 | topics: repoWithDetails.topics, 55 | }; 56 | if (yamlJson.bcgov_pubcode_version) { 57 | //backwards compatibility 58 | yamlJson.version = yamlJson.bcgov_pubcode_version; 59 | delete yamlJson.bcgov_pubcode_version; 60 | } 61 | 62 | return yamlJson; 63 | } 64 | 65 | const DAY_IN_MILLIS = 24 * 60 * 60 * 1000; 66 | 67 | /** 68 | * Fetches all the bcgovpubcode yaml files from the specified repos and converts them to JSON 69 | * @param compareLastUpdateDate 70 | * @returns {Promise<*[]>} 71 | */ 72 | async function getAllPubCodeYamlsAsJSON(compareLastUpdateDate) { 73 | const yamlArray = []; 74 | for (const repoWithDetails of repoWithDetailsArray) { 75 | //if date comparison is enabled for this workflow and last_updated is not within last 1 day skip 76 | if (compareLastUpdateDate) { 77 | const currentDate = new Date(); 78 | const lastUpdatedDate = new Date(repoWithDetails.lastUpdated); 79 | if (currentDate.getTime() - lastUpdatedDate.getTime() > DAY_IN_MILLIS) { 80 | console.debug( 81 | `Skipping ${repoWithDetails.name} repo as last updated date is more than 1 day.` 82 | ); 83 | continue; 84 | } 85 | } 86 | 87 | try { 88 | let yamlResponse; 89 | yamlResponse = await getYamlFromRepo( 90 | repoWithDetails.name, 91 | repoWithDetails.defaultBranch 92 | ); 93 | const yamlJson = processYamlFromHttpResponse( 94 | yamlResponse, 95 | repoWithDetails 96 | ); 97 | console.info(`found yaml file for ${repoWithDetails.name} repo.`); 98 | yamlArray.push(yamlJson); 99 | } catch (e) { 100 | console.debug( 101 | `Error while fetching yaml file for ${repoWithDetails.name} repo. Error: ${e.message}` 102 | ); 103 | } 104 | } 105 | return yamlArray; 106 | } 107 | 108 | /** 109 | * upload all the JSON to api 110 | * @param yamlArrayAsJson 111 | * @returns {Promise} 112 | */ 113 | async function bulkLoadPubCodes(yamlArrayAsJson) { 114 | if (yamlArrayAsJson.length > 0) { 115 | console.debug( 116 | `Found ${yamlArrayAsJson.length} yaml files to load into database.` 117 | ); 118 | //send to backend api bulk load endpoint 119 | try { 120 | await axios.post(`${API_URL}/api/pub-code/bulk-load`, yamlArrayAsJson, { 121 | headers: { 122 | "X-API-KEY": API_KEY, 123 | }, 124 | }); 125 | console.debug( 126 | `Successfully loaded ${yamlArrayAsJson.length} yaml files into database.` 127 | ); 128 | } catch (e) { 129 | console.error(e.response?.status); 130 | console.error(e.response?.config?.url); 131 | process.exit(1); 132 | } 133 | } else { 134 | console.debug( 135 | `No yaml files found at the root of repositories under bcgov.` 136 | ); 137 | } 138 | } 139 | 140 | /** 141 | * execute graphql query and return the response. 142 | * @param query 143 | * @returns {Promise<*|{}|number[]>} 144 | */ 145 | /** 146 | * Sends a GraphQL query to the GitHub API and handles rate limiting with exponential backoff. 147 | * 148 | * @async 149 | * @function getGraphQlResponseOnQuery 150 | * @param {string} query - The GraphQL query to send to the GitHub API 151 | * @returns {Promise} The response data from the GraphQL API 152 | * @throws {Error} Throws an error if all retry attempts fail or if a non-rate-limit error occurs 153 | * 154 | * @description 155 | * This function sends a GraphQL query to GitHub's API and implements: 156 | * - Authentication using a token 157 | * - Automatic retry for rate limit errors (up to 5 attempts) 158 | * - Exponential backoff with jitter to respect rate limits 159 | * - Detailed logging of retry attempts 160 | * https://docs.github.com/en/rest/using-the-rest-api/rate-limits-for-the-rest-api?apiVersion=2022-11-28#about-secondary-rate-limits 161 | * Retry delays increase exponentially, with random jitter added: 162 | */ 163 | async function getGraphQlResponseOnQuery(query) { 164 | const maxRetries = 5; 165 | let retries = 0; 166 | 167 | while (retries < maxRetries) { 168 | try { 169 | const response = await axios({ 170 | method: "post", 171 | url: "https://api.github.com/graphql", 172 | headers: { 173 | Authorization: `Bearer ${token}`, 174 | "Content-Type": "application/json", 175 | "Cache-Control": "no-cache", 176 | }, 177 | data: { 178 | query, 179 | }, 180 | }); 181 | return response.data; 182 | } catch (error) { 183 | if (error.response?.status === 403 && error.response?.data?.message?.includes("rate limit")) { 184 | retries++; 185 | if (retries >= maxRetries) throw error; 186 | 187 | 188 | 189 | // Calculate delay with exponential backoff and jitter 190 | const exponentialPart = BASE_DELAY * (2 ** retries); 191 | const jitterFactor = Math.random() * 0.3; // Random jitter between 0-30% 192 | const jitterAmount = exponentialPart * jitterFactor; 193 | const delay = Math.min(exponentialPart + jitterAmount, MAX_DELAY); 194 | // Example delay values (in milliseconds) per retry: 195 | // Retry 1: ~60000ms (1 min) + up to 18000ms jitter = ~78s 196 | // Retry 2: ~120000ms (2 min) + up to 36000ms jitter = ~156s 197 | // Retry 3: ~240000ms (4 min) + up to 72000ms jitter = ~312s 198 | // Retry 4: ~480000ms (8 min) + up to 144000ms jitter = ~624s 199 | // Retry 5: ~600000ms (10 min, capped, jitter ignored) = 10 min 200 | console.log(`Rate limit hit. Retrying in ${Math.round(delay/1000)} seconds... (Attempt ${retries}/${maxRetries})`); 201 | await new Promise(resolve => setTimeout(resolve, delay)); 202 | } else { 203 | throw error; 204 | } 205 | } 206 | } 207 | } 208 | 209 | /** 210 | * start crawling. 211 | * @returns {Promise} 212 | */ 213 | const performCrawling = async () => { 214 | let moreRecords = true; 215 | let cursor = ""; 216 | do { 217 | let after = ""; 218 | if (cursor) { 219 | after = `,after:"${cursor}"`; 220 | } 221 | const query = `query { 222 | organization(login: "bcgov") { 223 | repositories(first:100${after}){ 224 | edges{ 225 | node{ 226 | url, 227 | name, 228 | isArchived, 229 | repositoryTopics(first:20) { 230 | nodes { 231 | topic { 232 | name 233 | } 234 | } 235 | }, 236 | defaultBranchRef{ 237 | name 238 | }, 239 | stargazers { 240 | totalCount 241 | }, 242 | watchers { 243 | totalCount 244 | }, 245 | licenseInfo { 246 | name 247 | }, 248 | updatedAt, 249 | pushedAt 250 | } 251 | cursor 252 | } 253 | } 254 | } 255 | }`; 256 | try { 257 | const responseData = await getGraphQlResponseOnQuery(query); 258 | if (responseData.data?.organization?.repositories?.edges?.length > 0) { 259 | for (const edge of responseData.data.organization.repositories.edges) { 260 | if (edge.node?.defaultBranchRef?.name && !edge.node.isArchived) { 261 | repoWithDetailsArray.push({ 262 | name: edge.node.name, 263 | defaultBranch: edge.node.defaultBranchRef.name, 264 | stars: edge.node.stargazers?.totalCount, 265 | lastUpdated: edge.node.pushedAt, 266 | license: edge.node.licenseInfo?.name, 267 | watchers: edge.node.watchers?.totalCount, 268 | topics: edge.node.repositoryTopics.nodes.map( 269 | (node) => node.topic.name 270 | ), 271 | }); 272 | } else { 273 | console.warn( 274 | `skipping ${edge.node.name} as it does not have default branch or is archived., Default branch: '${edge.node.defaultBranchRef?.name}', isArchived: '${edge.node.isArchived}'` 275 | ); 276 | } 277 | cursor = edge.cursor; // keep overriding, the last cursor will be used 278 | } 279 | if (responseData.data.organization.repositories.edges?.length < 100) { 280 | moreRecords = false; 281 | } 282 | } else { 283 | moreRecords = false; 284 | } 285 | console.info("iteration completed, cursor at ", cursor); 286 | } catch (e) { 287 | console.error(e); 288 | } 289 | } while (moreRecords); 290 | const yamlAsJsons = await getAllPubCodeYamlsAsJSON(true); 291 | await bulkLoadPubCodes(yamlAsJsons); 292 | }; 293 | 294 | if (!token || !API_KEY || !API_URL) { 295 | console.error("Please provide GIT_TOKEN, API_KEY and API_URL in .env file"); 296 | process.exit(1); 297 | } else { 298 | console.info("Starting crawling... and API_URL is ", API_URL); 299 | } 300 | if (REPO_NAMES?.length > 0) { 301 | const repoNames = REPO_NAMES.split(","); 302 | for (const repoName of repoNames) { 303 | try { 304 | const query = `query { 305 | repository(owner: "bcgov", name: "${repoName}") { 306 | name, 307 | description, 308 | defaultBranchRef{ 309 | name 310 | }, 311 | repositoryTopics(first:20) { 312 | nodes { 313 | topic { 314 | name 315 | } 316 | } 317 | }, 318 | updatedAt, 319 | pushedAt, 320 | stargazers { 321 | totalCount 322 | }, 323 | watchers { 324 | totalCount 325 | }, 326 | licenseInfo { 327 | name 328 | }, 329 | } 330 | }`; 331 | const responseData = await getGraphQlResponseOnQuery(query); 332 | const repo = responseData.data?.repository; 333 | repoWithDetailsArray.push({ 334 | name: repo?.name, 335 | defaultBranch: repo?.defaultBranchRef?.name, 336 | stars: repo?.stargazers?.totalCount, 337 | lastUpdated: repo?.pushedAt, 338 | license: repo?.licenseInfo?.name, 339 | watchers: repo?.watchers?.totalCount, 340 | topics: repo?.repositoryTopics?.nodes.map((node) => node.topic.name), 341 | }); 342 | } catch (e) { 343 | console.error( 344 | `Error while fetching yaml file for ${repoName} repo. Error: ${e.message}` 345 | ); 346 | } 347 | } 348 | const yamlAsJsons = await getAllPubCodeYamlsAsJSON(false); 349 | await bulkLoadPubCodes(yamlAsJsons); 350 | } else { 351 | await performCrawling(); 352 | } 353 | -------------------------------------------------------------------------------- /database/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM ghcr.io/bcgov/nr-containers/mongo:6.0.11 2 | 3 | # Boilerplate to keep scanners happy (covered in FROM image) 4 | HEALTHCHECK CMD ["mongosh", "--eval", "db.adminCommand('ping')"] 5 | USER mongodb 6 | -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | --- 2 | version: "3.9" 3 | 4 | services: 5 | database: 6 | environment: 7 | MONGO_INITDB_ROOT_USERNAME: admin 8 | MONGO_INITDB_ROOT_PASSWORD: DPEPGHbjUluRjetU 9 | MONGODB_DATABASE: pubcode 10 | 11 | image: ghcr.io/bcgov/nr-containers/mongo:6.0.11 12 | ports: ["27017:27017"] 13 | restart: unless-stopped 14 | 15 | mongo-express: # The admin UI container for mongoDB 16 | image: mongo-express 17 | restart: unless-stopped 18 | ports: 19 | - "8001:8081" 20 | environment: 21 | ME_CONFIG_MONGODB_ENABLE_ADMIN: "true" 22 | ME_CONFIG_MONGODB_AUTH_DATABASE: db 23 | ME_CONFIG_MONGODB_AUTH_USERNAME: default 24 | ME_CONFIG_MONGODB_AUTH_PASSWORD: default 25 | ME_CONFIG_MONGODB_ADMINUSERNAME: default 26 | ME_CONFIG_MONGODB_ADMINPASSWORD: default 27 | ME_CONFIG_MONGODB_URL: mongodb://admin:DPEPGHbjUluRjetU@database:27017/ 28 | depends_on: 29 | database: 30 | condition: service_started 31 | 32 | api: 33 | image: pub-code-api:latest 34 | build: 35 | context: api 36 | environment: 37 | DB_HOST: database 38 | DB_USER: admin 39 | DB_PWD: DPEPGHbjUluRjetU 40 | DB_NAME: pubcode 41 | ports: 42 | - "3005:3000" 43 | restart: unless-stopped 44 | depends_on: 45 | database: 46 | condition: service_started 47 | 48 | #Run npm run build-watch in local before this. 49 | frontend: 50 | image: caddy:2.10.0-alpine 51 | working_dir: /app 52 | volumes: 53 | - ./frontend/Caddyfile:/etc/caddy/Caddyfile 54 | - ./frontend/dist:/app/dist 55 | depends_on: 56 | api: 57 | condition: service_started 58 | ports: 59 | - "3002:3000" 60 | restart: unless-stopped 61 | -------------------------------------------------------------------------------- /frontend/.dockerignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | cypress 3 | -------------------------------------------------------------------------------- /frontend/.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | pnpm-debug.log* 8 | lerna-debug.log* 9 | 10 | node_modules 11 | dist 12 | dist-ssr 13 | *.local 14 | 15 | # Editor directories and files 16 | .vscode/* 17 | !.vscode/extensions.json 18 | .idea 19 | .DS_Store 20 | *.suo 21 | *.ntvs* 22 | *.njsproj 23 | *.sln 24 | *.sw? 25 | -------------------------------------------------------------------------------- /frontend/Caddyfile: -------------------------------------------------------------------------------- 1 | { 2 | metrics 3 | auto_https off 4 | admin 0.0.0.0:3002 5 | } 6 | :3000 { 7 | log { 8 | output stdout 9 | format console { 10 | time_format iso8601 11 | level_format color 12 | } 13 | level {$LOG_LEVEL} 14 | } 15 | encode gzip 16 | 17 | handle /config.js { 18 | header Content-Type "text/javascript" 19 | respond `window.config = {"VITE_SCHEMA_BRANCH":"{$VITE_SCHEMA_BRANCH}", "VITE_POWERBI_URL":"{$VITE_POWERBI_URL}"};` 20 | } 21 | root * /app/dist 22 | encode zstd gzip 23 | file_server 24 | @spa_router { 25 | not path /api/* /config.js 26 | file { 27 | try_files {path} /index.html 28 | } 29 | } 30 | rewrite @spa_router {http.matchers.file.relative} 31 | header { 32 | -Server 33 | X-Frame-Options "SAMEORIGIN" 34 | X-XSS-Protection "1;mode=block" 35 | Cache-Control "no-store, no-cache, must-revalidate, proxy-revalidate" 36 | X-Content-Type-Options "nosniff" 37 | Strict-Transport-Security "max-age=31536000" 38 | Content-Security-Policy 39 | https://raw.githubusercontent.com/bcgov/* 40 | "default-src 'self' https://*.gov.bc.ca; 41 | script-src 'self' https://*.gov.bc.ca ; 42 | style-src 'self' https://fonts.googleapis.com https://use.fontawesome.com 'unsafe-inline'; 43 | font-src 'self' https://fonts.gstatic.com; 44 | img-src 'self' data: https://fonts.googleapis.com https://www.w3.org https://*.gov.bc.ca https://*.tile.openstreetmap.org; 45 | frame-ancestors 'self'; 46 | form-action 'self'; 47 | block-all-mixed-content; 48 | connect-src 'self' https://*.gov.bc.ca wss://*.gov.bc.ca;" 49 | Referrer-Policy "same-origin" 50 | Permissions-Policy "fullscreen=(self), camera=(), microphone=()" 51 | Cross-Origin-Resource-Policy "cross-origin" 52 | Cross-Origin-Opener-Policy "same-origin" 53 | } 54 | reverse_proxy /api* {$BACKEND_URL} 55 | } 56 | 57 | :3001 { 58 | handle /health { 59 | respond "OK" 60 | } 61 | } -------------------------------------------------------------------------------- /frontend/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM node:22 AS build 2 | 3 | WORKDIR /app 4 | COPY . . 5 | RUN npm ci --ignore-scripts && \ 6 | npm run build 7 | 8 | # Caddy 9 | FROM caddy:2.10-alpine 10 | 11 | # Copy static files and config 12 | COPY --from=build /app/dist /app/dist 13 | COPY Caddyfile /etc/caddy/Caddyfile 14 | 15 | # Packages and caddy format 16 | RUN apk add --no-cache ca-certificates && \ 17 | caddy fmt --overwrite /etc/caddy/Caddyfile 18 | 19 | # Port, health check and non-root user 20 | EXPOSE 3000 3001 21 | HEALTHCHECK --interval=30s --timeout=3s CMD curl -f http://localhost/:3001/health 22 | USER 1001 23 | -------------------------------------------------------------------------------- /frontend/README.md: -------------------------------------------------------------------------------- 1 | # React + Vite 2 | 3 | This template provides a minimal setup to get React working in Vite with HMR and some ESLint rules. 4 | 5 | Currently, two official plugins are available: 6 | 7 | - [@vitejs/plugin-react](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react/README.md) uses [Babel](https://babeljs.io/) for Fast Refresh 8 | - [@vitejs/plugin-react-swc](https://github.com/vitejs/vite-plugin-react-swc) uses [SWC](https://swc.rs/) for Fast Refresh 9 | -------------------------------------------------------------------------------- /frontend/_gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | pnpm-debug.log* 8 | lerna-debug.log* 9 | 10 | node_modules 11 | dist 12 | dist-ssr 13 | *.local 14 | 15 | # Editor directories and files 16 | .vscode/* 17 | !.vscode/extensions.json 18 | .idea 19 | .DS_Store 20 | *.suo 21 | *.ntvs* 22 | *.njsproj 23 | *.sln 24 | *.sw? 25 | -------------------------------------------------------------------------------- /frontend/cypress.config.js: -------------------------------------------------------------------------------- 1 | import cypress from 'cypress'; 2 | 3 | export default cypress.defineConfig({ 4 | e2e: { 5 | baseUrl: 'http://localhost:5173', 6 | setupNodeEvents(on, config) { 7 | // implement node event listeners here 8 | }, 9 | experimentalStudio: true, 10 | experimentalWebKitSupport: true, 11 | supportFile: false 12 | }, 13 | }); 14 | -------------------------------------------------------------------------------- /frontend/cypress/e2e/edit-form.cy.js: -------------------------------------------------------------------------------- 1 | describe("New Form", () => { 2 | beforeEach(() => { 3 | cy.visit("/edit"); 4 | }); 5 | 6 | it("renders the form", () => { 7 | cy.contains("GitHub Public Code Yml Link"); 8 | cy.get("#root_bcgovpubcode_url").should("exist").type("https://github.com/bcgov/public-code/blob/main/bcgovpubcode.yml"); 9 | cy.get("#submit").should("exist").click(); 10 | cy.get("#submit").should("exist").click(); 11 | cy.get("#copy").should("exist"); 12 | cy.get("#download").should("exist"); 13 | }); 14 | 15 | 16 | /*it("clicks on the New button and navigates to /form", () => { 17 | cy.get("#New").contains("New").click(); 18 | cy.location("pathname").should("eq", "/form"); 19 | }); 20 | 21 | it("clicks on the Edit button and navigates to /edit-form", () => { 22 | cy.get("#Edit").contains("Edit").click(); 23 | cy.location("pathname").should("eq", "/edit-form"); 24 | });*/ 25 | 26 | /* it("clicks on the Edit button and navigates to /edit-form", () => { 27 | cy.get(".leftDrawer Button").contains("Edit").click(); 28 | cy.location("pathname").should("eq", "/edit-form"); 29 | });*/ 30 | 31 | // Uncomment the following test if you want to test the disabled button scenario 32 | // it("verifies the disabled button", () => { 33 | // cy.get(".leftDrawer Button").contains("Search").should("be.disabled"); 34 | // }); 35 | }); 36 | -------------------------------------------------------------------------------- /frontend/cypress/e2e/home-page.cy.js: -------------------------------------------------------------------------------- 1 | describe("Home page visit", () => { 2 | 3 | it("visit landing page", () => { 4 | cy.visit("/"); 5 | cy.contains("BCGov Pubcode"); 6 | }); 7 | }); 8 | -------------------------------------------------------------------------------- /frontend/cypress/e2e/left-drawer.cy.js: -------------------------------------------------------------------------------- 1 | describe("LeftDrawer", () => { 2 | beforeEach(() => { 3 | cy.visit("/"); 4 | }); 5 | 6 | it("renders the drawer", () => { 7 | cy.get(".MuiDrawer-root").should("exist"); 8 | }); 9 | 10 | 11 | it("clicks on the New button and navigates to /form", () => { 12 | cy.get("#New").contains("New").click(); 13 | cy.location("pathname").should("eq", "/form"); 14 | }); 15 | 16 | it("clicks on the Edit button and navigates to /edit", () => { 17 | cy.get("#Edit").contains("Edit").click(); 18 | cy.location("pathname").should("eq", "/edit"); 19 | }); 20 | 21 | /* it("clicks on the Edit button and navigates to /edit-form", () => { 22 | cy.get(".leftDrawer Button").contains("Edit").click(); 23 | cy.location("pathname").should("eq", "/edit-form"); 24 | });*/ 25 | 26 | // Uncomment the following test if you want to test the disabled button scenario 27 | // it("verifies the disabled button", () => { 28 | // cy.get(".leftDrawer Button").contains("Search").should("be.disabled"); 29 | // }); 30 | }); 31 | -------------------------------------------------------------------------------- /frontend/cypress/e2e/new-form.cy.js: -------------------------------------------------------------------------------- 1 | describe("New Form", () => { 2 | beforeEach(() => { 3 | cy.visit("/form"); 4 | }); 5 | 6 | it("renders the form", () => { 7 | cy.contains("JSON schema for bcgovpubcode.yml"); 8 | cy.get("#root_product_information_ministry").should("exist").click().get("#menu-root_product_information_ministry > div.MuiPaper-root.MuiMenu-paper.MuiPopover-paper.MuiPaper-elevation8.MuiPaper-rounded > ul > li:nth-child(22)").click().trigger('keydown', { keyCode: 27, which: 27 }); 9 | cy.get("#root_product_information_program_area").should("exist").type("Water, Land and Resource Stewardship"); 10 | cy.get("#root_product_information_product_acronym").should("exist").type("Water, Land and Resource Stewardship"); 11 | cy.get("#root_product_information_product_name").should("exist").type("Water, Land and Resource Stewardship"); 12 | cy.get("#root_product_information_product_description").should("exist").type("Water, Land and Resource Stewardship"); 13 | cy.get("#root_data_management_roles_product_owner").should("exist").type("Test"); 14 | cy.get("#root_data_management_roles_data_custodian").should("exist").type("Test"); 15 | cy.get("#root_product_technology_information_hosting_platforms").should("exist").click().get("#menu-root_product_technology_information_hosting_platforms > div.MuiPaper-root.MuiMenu-paper.MuiPopover-paper.MuiPaper-elevation8.MuiPaper-rounded > ul > li:nth-child(2)").click().trigger('keydown', { keyCode: 27, which: 27 }); 16 | cy.get("#submit").should("exist").click(); 17 | cy.get("#copy").should("exist"); 18 | cy.get("#download").should("exist"); 19 | }); 20 | 21 | 22 | /*it("clicks on the New button and navigates to /form", () => { 23 | cy.get("#New").contains("New").click(); 24 | cy.location("pathname").should("eq", "/form"); 25 | }); 26 | 27 | it("clicks on the Edit button and navigates to /edit-form", () => { 28 | cy.get("#Edit").contains("Edit").click(); 29 | cy.location("pathname").should("eq", "/edit-form"); 30 | });*/ 31 | 32 | /* it("clicks on the Edit button and navigates to /edit-form", () => { 33 | cy.get(".leftDrawer Button").contains("Edit").click(); 34 | cy.location("pathname").should("eq", "/edit-form"); 35 | });*/ 36 | 37 | // Uncomment the following test if you want to test the disabled button scenario 38 | // it("verifies the disabled button", () => { 39 | // cy.get(".leftDrawer Button").contains("Search").should("be.disabled"); 40 | // }); 41 | }); 42 | -------------------------------------------------------------------------------- /frontend/cypress/fixtures/example.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Using fixtures to represent data", 3 | "email": "hello@cypress.io", 4 | "body": "Fixtures are a great way to mock data for responses to routes" 5 | } 6 | -------------------------------------------------------------------------------- /frontend/env.js: -------------------------------------------------------------------------------- 1 | 2 | 3 | // eslint-disable-next-line import/prefer-default-export 4 | export const env = { ...import.meta.env, ...window.config }; -------------------------------------------------------------------------------- /frontend/eslint.config.js: -------------------------------------------------------------------------------- 1 | import js from '@eslint/js' 2 | import globals from 'globals' 3 | import react from 'eslint-plugin-react' 4 | import reactHooks from 'eslint-plugin-react-hooks' 5 | import reactRefresh from 'eslint-plugin-react-refresh' 6 | 7 | export default [ 8 | { ignores: ['dist'] }, 9 | { 10 | files: ['**/*.{js,jsx}'], 11 | languageOptions: { 12 | ecmaVersion: 2020, 13 | globals: globals.browser, 14 | parserOptions: { 15 | ecmaVersion: 'latest', 16 | ecmaFeatures: { jsx: true }, 17 | sourceType: 'module', 18 | }, 19 | }, 20 | settings: { react: { version: '18.3' } }, 21 | plugins: { 22 | react, 23 | 'react-hooks': reactHooks, 24 | 'react-refresh': reactRefresh, 25 | }, 26 | rules: { 27 | ...js.configs.recommended.rules, 28 | ...react.configs.recommended.rules, 29 | ...react.configs['jsx-runtime'].rules, 30 | ...reactHooks.configs.recommended.rules, 31 | 'react/jsx-no-target-blank': 'off', 32 | 'react-refresh/only-export-components': [ 33 | 'warn', 34 | { allowConstantExport: true }, 35 | ], 36 | }, 37 | }, 38 | ] 39 | -------------------------------------------------------------------------------- /frontend/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | BCGov Public Code 8 | 9 | 10 | 11 |
12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /frontend/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "frontend", 3 | "private": true, 4 | "version": "0.0.0", 5 | "type": "module", 6 | "scripts": { 7 | "dev": "vite", 8 | "build": "vite build", 9 | "lint": "eslint .", 10 | "preview": "vite preview" 11 | }, 12 | "dependencies": { 13 | "@bcgov/design-system-react-components": "^0.5.1", 14 | "@emotion/react": "^11.14.0", 15 | "@emotion/styled": "^11.14.0", 16 | "@mui/icons-material": "^6.4.4", 17 | "@mui/material": "^6.4.4", 18 | "@rjsf/core": "^5.24.3", 19 | "@rjsf/mui": "^5.24.3", 20 | "@rjsf/utils": "^5.24.3", 21 | "@rjsf/validator-ajv8": "^5.24.3", 22 | "file-saver": "^2.0.5", 23 | "js-yaml": "^4.1.0", 24 | "react": "^19.0.0", 25 | "react-dom": "^19.0.0", 26 | "react-router": "^7.1.5", 27 | "react-syntax-highlighter": "^15.6.1", 28 | "react-toastify": "^11.0.3" 29 | }, 30 | "devDependencies": { 31 | "@eslint/js": "^9.19.0", 32 | "@types/react": "^19.0.8", 33 | "@types/react-dom": "^19.0.3", 34 | "@vitejs/plugin-react": "^4.6.0", 35 | "cypress": "^14.0.3", 36 | "eslint": "^9.19.0", 37 | "eslint-plugin-react": "^7.37.4", 38 | "eslint-plugin-react-hooks": "^5.0.0", 39 | "eslint-plugin-react-refresh": "^0.4.18", 40 | "globals": "^16.0.0", 41 | "vite": "^7.0.0" 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /frontend/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bcgov/pubcode/d20f86f24d2ee13cfc726d2412227eab6cb563f3/frontend/public/favicon.ico -------------------------------------------------------------------------------- /frontend/public/gov-bc-logo-horiz.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bcgov/pubcode/d20f86f24d2ee13cfc726d2412227eab6cb563f3/frontend/public/gov-bc-logo-horiz.ico -------------------------------------------------------------------------------- /frontend/src/App.css: -------------------------------------------------------------------------------- 1 | .appContent { 2 | padding: 1rem; 3 | margin-left: -10rem; 4 | max-width: 100%; 5 | box-sizing: border-box; 6 | overflow-x: auto; 7 | } 8 | 9 | .yamlContent { 10 | display: flex; 11 | flex-direction: column; 12 | gap: 1rem; 13 | } 14 | #yamlSyntaxHighlighter { 15 | margin-right: 1rem; 16 | } 17 | .customButton { 18 | margin-right: 1rem !important; 19 | } 20 | 21 | @media (max-width: 37.5rem) { 22 | .appContent { 23 | padding: 0.625rem; 24 | margin-left: -5rem; 25 | } 26 | 27 | .yamlContent { 28 | gap: 0.5rem; 29 | } 30 | 31 | .customButton { 32 | width: 100%; 33 | margin-right: 0 !important; 34 | margin-bottom: 0.5rem !important; 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /frontend/src/App.jsx: -------------------------------------------------------------------------------- 1 | import "@bcgov/bc-sans/css/BC_Sans.css"; 2 | import { Footer, Header } from "@bcgov/design-system-react-components"; 3 | import "./App.css"; 4 | import { Box } from "@mui/material"; 5 | import CssBaseline from "@mui/material/CssBaseline"; 6 | import { BrowserRouter } from "react-router"; 7 | import AppRoutes from "./routes/index.jsx"; 8 | import LeftDrawer from "./components/LeftDrawer.jsx"; 9 | import { ToastContainer } from "react-toastify"; 10 | function App() { 11 | return ( 12 | 13 | 14 | 15 |
16 | 17 | 18 | 26 | 27 | 36 | 37 | 38 | 39 | 40 |