diff --git a/.github/actions/get-app-version/action.yaml b/.github/actions/get-app-version/action.yaml new file mode 100644 index 0000000000..51e9560143 --- /dev/null +++ b/.github/actions/get-app-version/action.yaml @@ -0,0 +1,39 @@ +name: Get app version + +inputs: + project-path: + description: The root path of the app project + required: true + ref: + description: The commit reference to use as a starting point for the version + required: true + default: "HEAD" + +outputs: + version: + description: The app version + value: ${{ steps.get-version.outputs.version }} + +runs: + using: "composite" + steps: + - id: get-version + shell: bash + run: | + LATEST_APP_COMMIT=$(git rev-list -1 --abbrev-commit ${{ inputs.ref }} -- ${{ inputs.project-path }}) + + COMMIT_RELEASE_VERSION=$(git describe --tags --abbrev=0 $LATEST_APP_COMMIT 2>/dev/null) || true + + LATEST_RELEASE=$(git describe --tags --abbrev=0 ${{ inputs.reference }} 2>/dev/null) || true + + if [[ $COMMIT_RELEASE_VERSION == $LATEST_RELEASE ]]; then + TAG=$(git describe --tags --exact-match $LATEST_APP_COMMIT 2>/dev/null) || true + + if [[ -n $TAG ]]; then + echo "version=release/$TAG" >> $GITHUB_OUTPUT + else + echo "version=$LATEST_APP_COMMIT" >> $GITHUB_OUTPUT + fi + else + echo "version=release/$LATEST_RELEASE" >> $GITHUB_OUTPUT + fi diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index c992ef2a45..26af6882c1 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -11,11 +11,54 @@ jobs: name: Orchestrator runs-on: ubuntu-latest outputs: + should-build-app: ${{ steps.changed-api-files.outputs.any_changed == 'true' || steps.changed-selfserve-files.outputs.any_changed == 'true' || steps.changed-internal-files.outputs.any_changed == 'true' || null }} + should-build-docker: ${{ steps.changed-api-docker-files.outputs.any_changed == 'true' || steps.changed-selfserve-docker-files.outputs.any_changed == 'true' || steps.changed-internal-docker-files.outputs.any_changed == 'true' || null }} + should-build-api: ${{ steps.changed-api-files.outputs.any_changed == 'true' || null }} + should-build-selfserve: ${{ steps.changed-selfserve-files.outputs.any_changed == 'true' || null }} + should-build-internal: ${{ steps.changed-internal-files.outputs.any_changed == 'true' || null }} + should-build-api-docker: ${{ steps.changed-api-docker-files.outputs.any_changed == 'true' || steps.changed-api-files.outputs.any_changed == 'true' || null }} + should-build-selfserve-docker: ${{ steps.changed-selfserve-docker-files.outputs.any_changed == 'true' || steps.changed-selfserve-files.outputs.any_changed == 'true' || null }} + should-build-internal-docker: ${{ steps.changed-internal-docker-files.outputs.any_changed == 'true' || steps.changed-internal-files.outputs.any_changed == 'true' || null }} should-build-docs: ${{ steps.changed-website-files.outputs.any_changed == 'true' || null }} steps: - uses: actions/checkout@v4 with: fetch-depth: 0 + - uses: tj-actions/changed-files@v42 + id: changed-api-files + with: + files: | + app/api/** + # since_last_remote_commit: true + - uses: tj-actions/changed-files@v42 + id: changed-selfserve-files + with: + files: | + app/selfserve/** + # since_last_remote_commit: true + - uses: tj-actions/changed-files@v42 + id: changed-internal-files + with: + files: | + app/internal/** + - uses: tj-actions/changed-files@v42 + id: changed-api-docker-files + with: + files: | + infra/docker/api/** + # since_last_remote_commit: true + - uses: tj-actions/changed-files@v42 + id: changed-selfserve-docker-files + with: + files: | + infra/docker/selfserve/** + # since_last_remote_commit: true + - uses: tj-actions/changed-files@v42 + id: changed-internal-docker-files + with: + files: | + infra/docker/internal/** + # since_last_remote_commit: true - uses: tj-actions/changed-files@v42 id: changed-website-files with: @@ -34,3 +77,93 @@ jobs: deploy: false permissions: contents: write + + get-app-versions: + name: Get latest app version + needs: + - orchestrator + runs-on: ubuntu-latest + outputs: + api: ${{ steps.api-version.outputs.version }} + selfserve: ${{ steps.selfserve-version.outputs.version }} + internal: ${{ steps.internal-version.outputs.version }} + steps: + - uses: actions/checkout@v4 + with: + ref: ${{ github.event.pull_request.head.sha }} + fetch-depth: 0 + - id: api-version + uses: ./.github/actions/get-app-version + with: + project-path: app/api + - id: selfserve-version + uses: ./.github/actions/get-app-version + with: + project-path: app/selfserve + - id: internal-version + uses: ./.github/actions/get-app-version + with: + project-path: app/internal + - name: Add to summary + run: | + echo "#### App versions:" >> $GITHUB_STEP_SUMMARY + echo "**API**: \`${{ steps.api-version.outputs.version }}\`" >> $GITHUB_STEP_SUMMARY + echo "**Selfserve**: \`${{ steps.selfserve-version.outputs.version }}\`" >> $GITHUB_STEP_SUMMARY + echo "**Internal**: \`${{ steps.internal-version.outputs.version }}\`" >> $GITHUB_STEP_SUMMARY + + app: + name: App + concurrency: + group: app-${{ matrix.project }}-${{ needs.get-app-versions.outputs[matrix.project] }} + needs: + - orchestrator + - get-app-versions + if: ${{ needs.orchestrator.outputs.should-build-app || needs.orchestrator.outputs.should-build-docker }} + strategy: + fail-fast: false + matrix: + project: + - api + - selfserve + - internal + exclude: + - project: ${{ (needs.orchestrator.outputs.should-build-api || needs.orchestrator.outputs.should-build-api-docker) && 'ignored' || 'api' }} + - project: ${{ (needs.orchestrator.outputs.should-build-selfserve || needs.orchestrator.outputs.should-build-selfserve-docker) && 'ignored' || 'selfserve' }} + - project: ${{ (needs.orchestrator.outputs.should-build-internal || needs.orchestrator.outputs.should-build-internal-docker) && 'ignored' || 'internal' }} + uses: ./.github/workflows/php.yaml + with: + project: ${{ matrix.project }} + should-upload-artefact: ${{ !!needs.orchestrator.outputs[format('should-build-{0}-docker', matrix.project)] }} + artefact-name: app-${{ matrix.project}}-${{ needs.get-app-versions.outputs[matrix.project] }} + retention-days: 1 + permissions: + contents: read + + docker: + name: Docker + concurrency: + group: docker-${{ matrix.project }}-${{ needs.get-app-versions.outputs[matrix.project] }} + needs: + - orchestrator + - get-app-versions + - app + if: ${{ always() && !cancelled() && !failure() && needs.orchestrator.outputs.should-build-docker }} + strategy: + fail-fast: false + matrix: + project: + - api + - selfserve + - internal + exclude: + - project: ${{ needs.orchestrator.outputs.should-build-api-docker && 'ignored' || 'api' }} + - project: ${{ needs.orchestrator.outputs.should-build-selfserve-docker && 'ignored' || 'selfserve' }} + - project: ${{ needs.orchestrator.outputs.should-build-internal-docker && 'ignored' || 'internal' }} + uses: ./.github/workflows/docker.yaml + with: + project: ${{ matrix.project }} + app-artefact-name: app-${{ matrix.project}}-${{ needs.get-app-versions.outputs[matrix.project] }} + should-upload-artefact-to-ecr: false + permissions: + contents: read + id-token: write diff --git a/.github/workflows/docker.yaml b/.github/workflows/docker.yaml new file mode 100644 index 0000000000..35a80ed9e0 --- /dev/null +++ b/.github/workflows/docker.yaml @@ -0,0 +1,67 @@ +name: Docker + +on: + workflow_call: + inputs: + ref: + type: string + required: false + project: + type: string + required: true + should-upload-artefact-to-ecr: + type: boolean + required: true + default: false + app-artefact-name: + type: string + required: true + +jobs: + check-ecr: + name: Check ECR + if: ${{ inputs.should-upload-artefact-to-ecr }} + runs-on: ubuntu-latest + outputs: + image-exists: ${{ steps.check-ecr.outputs.exists }} + env: + PROJECT: ${{ inputs.project }} + OBJECT_PREFIX: ${{ inputs.app-artefact-name }} + steps: + - name: Configure AWS credentials + uses: aws-actions/configure-aws-credentials@v4 + with: + role-to-assume: ${{ vars.TF_OIDC_ROLE }} + aws-region: ${{ vars.TF_AWS_REGION }} + - name: Check if image already exists in ECR + id: check-ecr + # Check if the image already exists in ECR, so we don't have to build it again. + run: exit 0 + + lint: + name: Lint + needs: + - check-ecr + runs-on: ubuntu-latest + if: ${{ always() && (needs.check-ecr.result == 'skipped' || !needs.check-ecr.outputs.image-exists) }} + steps: + - name: Lint + run: exit 0 + + build: + name: Build + needs: + - check-ecr + runs-on: ubuntu-latest + if: ${{ always() && (needs.check-ecr.result == 'skipped' || !needs.check-ecr.outputs.image-exists) }} + steps: + - uses: actions/checkout@v4 + with: + ref: ${{ inputs.ref || null }} + path: infra/docker/${{ inputs.project }} + - uses: actions/download-artifact@v4 + with: + name: ${{ inputs.app-artefact-name }} + path: app/${{ inputs.project }} + - name: Build + run: exit 0 diff --git a/.github/workflows/php.yaml b/.github/workflows/php.yaml new file mode 100644 index 0000000000..28b7adaa90 --- /dev/null +++ b/.github/workflows/php.yaml @@ -0,0 +1,191 @@ +name: PHP + +on: + workflow_call: + inputs: + ref: + type: string + required: false + project: + type: string + required: true + artefact-name: + type: string + required: false + should-upload-artefact: + type: boolean + required: false + default: false + retention-days: + type: number + required: false + default: 7 + +jobs: + warm-cache: + name: Warm cache + runs-on: ubuntu-latest + defaults: + run: + working-directory: app/${{ inputs.project }} + env: + # Temporary until this repository becomes a mono-repository: https://dvsa.atlassian.net/browse/VOL-4961. + REMOTE_REPOSITORY: ${{ inputs.project == 'api' && 'dvsa/olcs-backend' || format('dvsa/olcs-{0}', inputs.project) }} + steps: + - uses: actions/checkout@v4 + with: + repository: ${{ env.REMOTE_REPOSITORY }} + ref: ${{ inputs.ref || null }} + path: app/${{ inputs.project }} + - name: Setup PHP + uses: shivammathur/setup-php@v2 + with: + php-version: "7.4" + coverage: none + extensions: intl, pdo_mysql, redis, mbstring + - name: Get composer cache directory + id: composer-cache + run: echo "dir=$(composer config cache-files-dir)" >> $GITHUB_OUTPUT + - name: Cache Composer packages + uses: actions/cache@v4 + with: + path: ${{ steps.composer-cache.outputs.dir }} + key: ${{ runner.os }}-composer-${{ hashFiles(format('**/app/{0}/composer.lock', inputs.project)) }} + restore-keys: ${{ runner.os }}-composer- + - name: Install dependencies + run: composer install --no-progress --prefer-dist --optimize-autoloader + + test: + name: Test + needs: + - warm-cache + runs-on: ubuntu-latest + defaults: + run: + working-directory: app/${{ inputs.project }} + env: + # Temporary until this repository becomes a mono-repository. + REMOTE_REPOSITORY: ${{ inputs.project == 'api' && 'dvsa/olcs-backend' || format('dvsa/olcs-{0}', inputs.project) }} + steps: + - uses: actions/checkout@v4 + with: + repository: ${{ env.REMOTE_REPOSITORY }} + ref: ${{ inputs.ref || null }} + path: app/${{ inputs.project }} + - name: Setup PHP + uses: shivammathur/setup-php@v2 + with: + php-version: "7.4" + coverage: none + extensions: intl, pdo_mysql, redis, mbstring + - name: Get composer cache directory + id: composer-cache + run: echo "dir=$(composer config cache-files-dir)" >> $GITHUB_OUTPUT + - name: Cache Composer packages + uses: actions/cache@v4 + with: + path: ${{ steps.composer-cache.outputs.dir }} + key: ${{ runner.os }}-composer-${{ hashFiles(format('**/app/{0}/composer.lock', inputs.project)) }} + restore-keys: ${{ runner.os }}-composer- + - name: Install dependencies + run: composer install --no-progress --prefer-dist --optimize-autoloader + - name: Test + run: vendor/bin/phpunit + + # static-analysis: + # name: ${{ matrix.task.description }} + # needs: + # - warm-cache + # runs-on: ubuntu-latest + # strategy: + # fail-fast: false + # matrix: + # task: + # - description: PHPStan + # command: phpstan analyze --no-progress + # tools: phpstan + # - description: PHP CodeSniffer + # command: phpcs -q + # tools: phpcs + # - description: Psalm + # command: psalm --no-progress --output-format=github + # tools: psalm + # # Remove once: https://dvsa.atlassian.net/browse/VOL-4787 & https://dvsa.atlassian.net/browse/VOL-4788 are resolved. + # continue-on-error: true + # defaults: + # run: + # working-directory: app/${{ inputs.project }} + # env: + # # Temporary until this repository becomes a mono-repository: https://dvsa.atlassian.net/browse/VOL-4961. + # REMOTE_REPOSITORY: ${{ inputs.project == 'api' && 'dvsa/olcs-backend' || format('dvsa/olcs-{0}', inputs.project) }} + # steps: + # - uses: actions/checkout@v4 + # with: + # repository: ${{ env.REMOTE_REPOSITORY }} + # ref: ${{ inputs.ref || null }} + # path: app/${{ inputs.project }} + # - name: Setup PHP + # uses: shivammathur/setup-php@v2 + # with: + # php-version: "7.4" + # coverage: none + # extensions: intl, pdo_mysql, redis, mbstring + # tools: ${{ matrix.task.tools }} + # - name: Get composer cache directory + # id: composer-cache + # run: echo "dir=$(composer config cache-files-dir)" >> $GITHUB_OUTPUT + # - name: Cache Composer packages + # uses: actions/cache@v4 + # with: + # path: ${{ steps.composer-cache.outputs.dir }} + # key: ${{ runner.os }}-composer-${{ hashFiles(format('**/app/{0}/composer.lock', inputs.project)) }} + # restore-keys: ${{ runner.os }}-composer- + # - name: Install Composer dependencies + # run: composer install --no-progress --no-interaction + # - name: Execute ${{ matrix.task.description }} + # run: ${{ matrix.task.command }} + + package: + name: Package + needs: + - test + #- static-analysis + runs-on: ubuntu-latest + defaults: + run: + working-directory: app/${{ inputs.project }} + env: + # Temporary until this repository becomes a mono-repository: https://dvsa.atlassian.net/browse/VOL-4961. + REMOTE_REPOSITORY: ${{ inputs.project == 'api' && 'dvsa/olcs-backend' || format('dvsa/olcs-{0}', inputs.project) }} + steps: + - uses: actions/checkout@v4 + with: + repository: ${{ env.REMOTE_REPOSITORY }} + ref: ${{ inputs.ref || null }} + path: app/${{ inputs.project }} + - name: Setup PHP + uses: shivammathur/setup-php@v2 + with: + php-version: "7.4" + coverage: none + extensions: intl, pdo_mysql, redis, mbstring + - name: Get composer cache directory + id: composer-cache + run: echo "dir=$(composer config cache-files-dir)" >> $GITHUB_OUTPUT + - name: Cache Composer packages + uses: actions/cache@v4 + with: + path: ${{ steps.composer-cache.outputs.dir }} + key: ${{ runner.os }}-composer-${{ hashFiles(format('**/app/{0}/composer.lock', inputs.project)) }} + restore-keys: ${{ runner.os }}-composer- + - name: Install dependencies + run: composer install --no-progress --prefer-dist --no-dev --optimize-autoloader + - name: Archive + run: git archive --output=${{ inputs.artefact-name }}.tar.gz HEAD + - name: Upload artefact + if: ${{ inputs.should-upload-artefact }} + uses: actions/upload-artifact@v4 + with: + name: ${{ inputs.artefact-name }} + path: app/${{ inputs.project }}/${{ inputs.artefact-name }}.tar.gz + retention-days: ${{ inputs.retention-days }} diff --git a/infra/docker/api/Dockerfile b/infra/docker/api/Dockerfile new file mode 100644 index 0000000000..aafda6f37a --- /dev/null +++ b/infra/docker/api/Dockerfile @@ -0,0 +1 @@ +FROM php:8.2-fpm diff --git a/infra/docker/internal/Dockerfile b/infra/docker/internal/Dockerfile new file mode 100644 index 0000000000..aafda6f37a --- /dev/null +++ b/infra/docker/internal/Dockerfile @@ -0,0 +1 @@ +FROM php:8.2-fpm diff --git a/infra/docker/selfserve/Dockerfile b/infra/docker/selfserve/Dockerfile new file mode 100644 index 0000000000..aafda6f37a --- /dev/null +++ b/infra/docker/selfserve/Dockerfile @@ -0,0 +1 @@ +FROM php:8.2-fpm