diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml new file mode 100644 index 0000000..d90c2e3 --- /dev/null +++ b/.github/workflows/deploy.yml @@ -0,0 +1,106 @@ +name: Deploy + +on: + workflow_dispatch: + inputs: + environment: + description: "Target environment" + required: true + default: "staging" + type: choice + options: + - staging + - prod + image_tag: + description: "Image tag to deploy (defaults to sha-)" + required: false + default: "" + +jobs: + deploy: + runs-on: ubuntu-latest + concurrency: + group: developer-portal-deploy-${{ inputs.environment }} + cancel-in-progress: false + permissions: + contents: write + env: + ARGOCD_SERVER: ${{ secrets.ARGOCD_SERVER }} + ARGOCD_AUTH_TOKEN: ${{ secrets.ARGOCD_AUTH_TOKEN }} + + steps: + - name: Checkout + uses: actions/checkout@v6 + with: + fetch-depth: 0 + + - name: Resolve deployment metadata + run: | + set -euo pipefail + env_name="${{ inputs.environment }}" + image_tag="${{ inputs.image_tag }}" + if [[ -z "${image_tag}" ]]; then + image_tag="sha-$(git rev-parse --short=12 HEAD)" + fi + + if [[ "${env_name}" == "prod" ]]; then + app_name="developer-portal" + else + app_name="developer-portal-staging" + fi + + if [[ ! "${image_tag}" =~ ^[A-Za-z0-9._-]+$ ]]; then + echo "invalid image tag: ${image_tag}" >&2 + exit 1 + fi + + { + echo "DEPLOY_ENV=${env_name}" + echo "IMAGE_TAG=${image_tag}" + echo "ARGOCD_APP=${app_name}" + echo "GITOPS_CHANGED=false" + } >> "$GITHUB_ENV" + + - name: Update kustomize image tag + run: | + python3 - <<'PY' + from pathlib import Path + import os + import re + + env_name = os.environ["DEPLOY_ENV"] + image_tag = os.environ["IMAGE_TAG"] + path = Path(f"k8s/{env_name}/kustomization.yaml") + text = path.read_text() + updated = re.sub(r"(newTag:\s*)(\S+)", rf"\1{image_tag}", text, count=1) + if updated == text: + raise SystemExit("failed to update image tag in " + str(path)) + path.write_text(updated) + PY + + - name: Commit GitOps change + run: | + set -euo pipefail + git config user.name "github-actions[bot]" + git config user.email "41898282+github-actions[bot]@users.noreply.github.com" + git add "k8s/${DEPLOY_ENV}/kustomization.yaml" + if git diff --cached --quiet; then + echo "No GitOps change to commit." + echo "GITOPS_CHANGED=false" >> "$GITHUB_ENV" + exit 0 + fi + echo "GITOPS_CHANGED=true" >> "$GITHUB_ENV" + git commit -m "deploy(${DEPLOY_ENV}): developer-portal ${IMAGE_TAG}" + git push origin "HEAD:${{ github.ref_name }}" + + - name: Install Argo CD CLI + if: ${{ env.GITOPS_CHANGED == 'true' && env.ARGOCD_SERVER != '' && env.ARGOCD_AUTH_TOKEN != '' }} + run: | + curl -fsSL -o /tmp/argocd https://github.com/argoproj/argo-cd/releases/latest/download/argocd-linux-amd64 + install -m 0755 /tmp/argocd /usr/local/bin/argocd + + - name: Trigger Argo CD sync + if: ${{ env.GITOPS_CHANGED == 'true' && env.ARGOCD_SERVER != '' && env.ARGOCD_AUTH_TOKEN != '' }} + run: | + argocd --server "${ARGOCD_SERVER}" --auth-token "${ARGOCD_AUTH_TOKEN}" --grpc-web app sync "${ARGOCD_APP}" + argocd --server "${ARGOCD_SERVER}" --auth-token "${ARGOCD_AUTH_TOKEN}" --grpc-web app wait "${ARGOCD_APP}" --health --sync diff --git a/.github/workflows/elixir-quality.yml b/.github/workflows/elixir-quality.yml new file mode 100644 index 0000000..3b633d9 --- /dev/null +++ b/.github/workflows/elixir-quality.yml @@ -0,0 +1,65 @@ +name: Elixir Quality + +permissions: + contents: read + pull-requests: write + statuses: write + +on: + push: + branches: + - main + paths: + - 'lib/**' + - 'config/**' + - 'test/**' + - 'mix.exs' + - 'mix.lock' + - '.formatter.exs' + - '.credo.exs' + - '.credo.base.exs' + - '.credo.ex_dna.exs' + - '.credo.ex_slop.exs' + - '.sobelow-conf' + - '.deps_audit_ignore' + - '.tool-versions' + - 'scripts/elixir_quality.sh' + - '.github/workflows/elixir-quality.yml' + pull_request: + branches: + - main + paths: + - 'lib/**' + - 'config/**' + - 'test/**' + - 'mix.exs' + - 'mix.lock' + - '.formatter.exs' + - '.credo.exs' + - '.credo.base.exs' + - '.credo.ex_dna.exs' + - '.credo.ex_slop.exs' + - '.sobelow-conf' + - '.deps_audit_ignore' + - '.tool-versions' + - 'scripts/elixir_quality.sh' + - '.github/workflows/elixir-quality.yml' + +jobs: + quality: + name: Elixir Quality + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v6 + + - name: Run Elixir quality contract + run: | + docker run --rm \ + -v "${PWD}:${PWD}" \ + -w "${PWD}" \ + -e HEX_HOME=/tmp/hex \ + -e MIX_HOME=/tmp/mix \ + -e MIX_BUILD_PATH=/tmp/developer_portal_quality/_build \ + docker.io/library/elixir:1.19.4-otp-28 \ + bash -lc './scripts/elixir_quality.sh --project . --phoenix --skip-dialyzer' diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml new file mode 100644 index 0000000..02d3ca0 --- /dev/null +++ b/.github/workflows/main.yml @@ -0,0 +1,62 @@ +name: CI + +on: + push: + branches: + - main + pull_request: + +jobs: + build: + runs-on: ubuntu-latest + concurrency: + group: developer-portal-ci-${{ github.ref }} + cancel-in-progress: true + permissions: + contents: read + env: + BUILDBUDDY_ORG_API_KEY: ${{ secrets.BUILDBUDDY_ORG_API_KEY }} + BAZELISK_HOME: ${{ github.workspace }}/.cache/bazelisk + + steps: + - name: Checkout + uses: actions/checkout@v6 + + - name: Install Bazelisk + run: ./scripts/install-bazelisk.sh + + - name: Install OpenSpec CLI + run: npm install --global @fission-ai/openspec + + - name: Verify Bazel version + run: | + set -euo pipefail + expected="$(tr -d '\r\n' < .bazelversion)" + actual="$(bazel --version | awk '{print $NF}')" + test "${actual}" = "${expected}" + + - name: Configure BuildBuddy remote settings + run: ./scripts/write_buildbuddy_bazelrc.sh --require-key + + - name: Bazel build + run: bazel build --config=no_remote --verbose_failures //:compile //:release_tar //docker:developer_portal_image + + - name: Bazel test + run: bazel test --config=no_remote --verbose_failures //:mix_test + + - name: OpenSpec validation + run: | + set -euo pipefail + openspec validate --specs --strict + + for change in openspec/changes/*; do + if [ ! -d "${change}" ]; then + continue + fi + + if [ "$(basename "${change}")" = "archive" ]; then + continue + fi + + openspec validate "$(basename "${change}")" --strict + done diff --git a/.github/workflows/publish-image.yml b/.github/workflows/publish-image.yml new file mode 100644 index 0000000..be41487 --- /dev/null +++ b/.github/workflows/publish-image.yml @@ -0,0 +1,74 @@ +name: Publish Image + +on: + push: + branches: + - main + paths: + - 'assets/**' + - 'build/**' + - 'config/**' + - 'docker/**' + - 'lib/**' + - 'mix.exs' + - 'mix.lock' + - 'MODULE.bazel' + - 'BUILD.bazel' + - '.bazelrc' + - '.bazelversion' + - 'buildbuddy.yaml' + - 'rel/**' + - 'priv/**' + - 'scripts/**' + workflow_dispatch: + +jobs: + publish: + runs-on: ubuntu-latest + concurrency: + group: developer-portal-publish-${{ github.ref }} + cancel-in-progress: true + permissions: + contents: read + env: + BUILDBUDDY_ORG_API_KEY: ${{ secrets.BUILDBUDDY_ORG_API_KEY }} + OCI_REGISTRY: registry.carverauto.dev + OCI_USERNAME: ${{ secrets.HARBOR_ROBOT_USERNAME }} + OCI_TOKEN: ${{ secrets.HARBOR_ROBOT_SECRET }} + DOCKERHUB_USERNAME: ${{ secrets.DOCKERHUB_USERNAME }} + DOCKERHUB_TOKEN: ${{ secrets.DOCKERHUB_TOKEN }} + BAZELISK_HOME: ${{ github.workspace }}/.cache/bazelisk + + steps: + - name: Checkout + uses: actions/checkout@v6 + + - name: Install Bazelisk + run: ./scripts/install-bazelisk.sh + + - name: Verify Bazel version + run: | + set -euo pipefail + expected="$(tr -d '\r\n' < .bazelversion)" + actual="$(bazel --version | awk '{print $NF}')" + test "${actual}" = "${expected}" + + - name: Configure BuildBuddy remote settings + run: ./scripts/write_buildbuddy_bazelrc.sh --require-key + + - name: Verify publish credentials + run: | + set -euo pipefail + if [[ -z "${OCI_USERNAME:-}" || -z "${OCI_TOKEN:-}" ]]; then + echo "OCI_USERNAME and OCI_TOKEN secrets must be configured for image publishing." >&2 + exit 1 + fi + + - name: Configure registry auth for rules_oci + run: ./buildbuddy_setup_docker_auth.sh + + - name: Bazel test + run: bazel test --config=no_remote --verbose_failures //:mix_test + + - name: Build and publish image + run: bazel run --config=no_remote --verbose_failures //docker:developer_portal_image_push diff --git a/README.md b/README.md index 47cf112..2b07de8 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,7 @@ # ServiceRadar Developer Portal +Source of truth: https://github.com/carverauto/developer + Phoenix/LiveView application for `developer.serviceradar.cloud`, plus the OpenSpec and Kubernetes GitOps scaffolding for the developer portal. ## Local Development @@ -26,6 +28,11 @@ The app requires the usual Phoenix runtime config in deployed environments: Kubernetes manifests live under `k8s/` with `base`, `staging`, and `prod` overlays. The deployment derives DB access from the shared `developer-portal-db-credentials` CNPG bootstrap secret plus `PG*` config in `developer-portal-config`, and uses `developer-portal-secrets` for `SECRET_KEY_BASE`. Argo CD also gets a PreSync migration Job from `k8s/base/migration-job.yaml` so schema changes run before the Deployment rolls. See [k8s/README.md](/home/mfreeman/src/community/k8s/README.md). +CI, image publish, and deploy run on GitHub Actions (`.github/workflows`). +Required repository secrets: `BUILDBUDDY_ORG_API_KEY`, `HARBOR_ROBOT_USERNAME`, +`HARBOR_ROBOT_SECRET`, `DOCKERHUB_USERNAME`, `DOCKERHUB_TOKEN`, +`ARGOCD_SERVER`, `ARGOCD_AUTH_TOKEN`. + ## Build and Release Bazel is the primary build interface for this repository. Local and CI workflows should use: diff --git a/k8s/argocd-application-staging.yaml b/k8s/argocd-application-staging.yaml index 2e4c0f2..8d5a7da 100644 --- a/k8s/argocd-application-staging.yaml +++ b/k8s/argocd-application-staging.yaml @@ -8,7 +8,7 @@ metadata: spec: project: default source: - repoURL: https://git.carverauto.dev/carverauto/developer.git + repoURL: https://github.com/carverauto/developer.git targetRevision: HEAD path: k8s/staging destination: diff --git a/k8s/argocd-application.yaml b/k8s/argocd-application.yaml index d0ce07f..e4fd537 100644 --- a/k8s/argocd-application.yaml +++ b/k8s/argocd-application.yaml @@ -8,7 +8,7 @@ metadata: spec: project: default source: - repoURL: https://git.carverauto.dev/carverauto/developer.git + repoURL: https://github.com/carverauto/developer.git targetRevision: HEAD path: k8s/prod destination: