diff --git a/.github/workflows/build-frontend.yml b/.github/workflows/build-frontend.yml new file mode 100644 index 0000000..ffcdfdf --- /dev/null +++ b/.github/workflows/build-frontend.yml @@ -0,0 +1,170 @@ +name: Frontend - Build and Push + +on: + pull_request: + branches: + - pre + - main + push: + branches: + - pre + workflow_dispatch: + inputs: + publish_pre: + description: "Build and push the PRE image to ECR" + required: true + default: false + type: boolean + +permissions: + id-token: write + contents: read + +env: + AWS_REGION: eu-west-1 + ECR_REPOSITORY_PRE: gesplan-pre/vestimenta-frontend + NODE_VERSION: "20" + DOCKERFILE_PATH: Dockerfile + +jobs: + validate: + name: Validate frontend build + runs-on: ubuntu-latest + if: github.event_name == 'pull_request' || github.event_name == 'workflow_dispatch' + + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Validate repository structure + shell: bash + run: | + test -f package.json + test -f angular.json + test -d src + test -f "${DOCKERFILE_PATH}" + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: ${{ env.NODE_VERSION }} + cache: npm + + - name: Install dependencies + run: npm ci + + - name: Validate npm scripts + shell: bash + run: | + npm run | grep -qE '^ build$' + if npm run | grep -qE '^ lint$'; then + echo "HAS_LINT=true" >> "$GITHUB_ENV" + else + echo "HAS_LINT=false" >> "$GITHUB_ENV" + fi + if npm run | grep -qE '^ test$'; then + echo "HAS_TEST=true" >> "$GITHUB_ENV" + else + echo "HAS_TEST=false" >> "$GITHUB_ENV" + fi + + - name: Build Angular application + run: npm run build -- --configuration production + + - name: Run lint + if: env.HAS_LINT == 'true' + run: npm run lint + + - name: Run tests + if: env.HAS_TEST == 'true' + run: npm test -- --watch=false --browsers=ChromeHeadless + + - name: Build Docker image + run: docker build -f "${DOCKERFILE_PATH}" -t vestimenta-frontend:ci . + + - name: Workflow summary + shell: bash + run: | + { + echo "### Frontend CI validation" + echo "" + echo "- Angular build validated." + echo "- Docker image build validated with \`${DOCKERFILE_PATH}\`." + echo "- No ECR login or image push was executed." + echo "- No Helm, kubectl, EKS deploy, Terraform apply or destroy was executed." + } >> "$GITHUB_STEP_SUMMARY" + + build-and-push-pre: + name: Build and push PRE image + runs-on: ubuntu-latest + if: github.event_name == 'push' || (github.event_name == 'workflow_dispatch' && inputs.publish_pre == true) + outputs: + image_tag: ${{ steps.image.outputs.image_tag }} + + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Validate repository structure + shell: bash + run: | + test -f package.json + test -f angular.json + test -d src + test -f "${DOCKERFILE_PATH}" + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: ${{ env.NODE_VERSION }} + cache: npm + + - name: Install dependencies + run: npm ci + + - name: Validate npm build script + run: npm run | grep -qE '^ build$' + + - name: Build Angular application + run: npm run build -- --configuration production + + - name: Configure AWS credentials PRE + uses: aws-actions/configure-aws-credentials@v4 + with: + role-to-assume: ${{ secrets.AWS_ROLE_ARN_PRE }} + aws-region: ${{ env.AWS_REGION }} + + - name: Login to Amazon ECR + uses: aws-actions/amazon-ecr-login@v2 + + - name: Build and push Docker image + id: image + shell: bash + run: | + IMAGE_TAG="sha-${GITHUB_SHA}" + AWS_ACCOUNT_ID=$(aws sts get-caller-identity --query Account --output text) + ECR_REGISTRY="${AWS_ACCOUNT_ID}.dkr.ecr.${AWS_REGION}.amazonaws.com" + IMAGE="${ECR_REGISTRY}/${ECR_REPOSITORY_PRE}:${IMAGE_TAG}" + + docker build -f "${DOCKERFILE_PATH}" -t "${IMAGE}" . + docker push "${IMAGE}" + + echo "IMAGE_TAG=${IMAGE_TAG}" >> "$GITHUB_ENV" + echo "image_tag=${IMAGE_TAG}" >> "$GITHUB_OUTPUT" + echo "IMAGE=${IMAGE}" >> "$GITHUB_ENV" + + - name: Workflow summary + shell: bash + run: | + { + echo "### Frontend image published" + echo "" + echo "- Image: \`${IMAGE}\`" + echo "- Tag: \`${IMAGE_TAG}\`" + echo "- Repository: \`${ECR_REPOSITORY_PRE}\`" + echo "- Region: \`${AWS_REGION}\`" + echo "" + echo "Imagen publicada en ECR. Para desplegar, actualizar el tag correspondiente en infra-apps y ejecutar el workflow de despliegue desde infra-apps." + echo "" + echo "No se ha ejecutado Helm, kubectl, despliegue en EKS, Terraform apply ni Terraform destroy desde este repositorio." + } >> "$GITHUB_STEP_SUMMARY" diff --git a/docs/appVestimenta/pre-frontend.md b/docs/appVestimenta/pre-frontend.md index 6ec6c57..f5cdbeb 100644 --- a/docs/appVestimenta/pre-frontend.md +++ b/docs/appVestimenta/pre-frontend.md @@ -1,10 +1,87 @@ # appVestimenta PRE — Frontend Angular +## CI/CD + +El repositorio `appVestimenta` construye y publica la imagen Docker del frontend, pero no despliega en EKS. + +Workflow: + +`.github/workflows/build-frontend.yml` + +Nombre: + +`Frontend - Build and Push` + +Modelo operativo: + +- `pull_request` hacia `pre` o `main`: valida Angular, tests si existen y build Docker sin publicar imagen. +- `push` a `pre`: compila, construye la imagen y la publica en ECR PRE. +- `workflow_dispatch`: valida manualmente y permite publicar PRE si `publish_pre=true`. +- El despliegue Helm/Kubernetes se realiza desde `infra-apps`. + +El workflow no ejecuta `helm`, `kubectl`, Terraform ni despliegues directos a EKS. + ## Imagen -ECR: +ECR PRE: + +`674624358677.dkr.ecr.eu-west-1.amazonaws.com/gesplan-pre/vestimenta-frontend` + +Repositorio lógico: + +`gesplan-pre/vestimenta-frontend` + +Tag obligatorio: + +`sha-` + +Ejemplo: + +`674624358677.dkr.ecr.eu-west-1.amazonaws.com/gesplan-pre/vestimenta-frontend:sha-8f8d02b...` + +No usar `latest` como tag de despliegue. El tag que debe consumir `infra-apps` es siempre el `sha-` generado por el workflow. + +## Autenticación AWS + +La autenticación con AWS se hace mediante GitHub Actions OIDC. -`674624358677.dkr.ecr.eu-west-1.amazonaws.com/gesplan-pre/vestimenta-frontend:pre` +Secret requerido: + +`AWS_ROLE_ARN_PRE` + +No configurar `AWS_ACCESS_KEY_ID` ni `AWS_SECRET_ACCESS_KEY` en este repositorio. + +## Build frontend + +Proyecto Angular detectado por: + +- `package.json` +- `angular.json` +- `src/` + +Dockerfile: + +`Dockerfile` + +Scripts npm detectados: + +- `build`: `ng build` +- `test`: `ng test` + +No hay script `lint` definido actualmente. + +## API + +La configuración objetivo para consumir el backend es usar ruta relativa: + +`/api` + +Estado actual detectado: + +- `src/app/usuarios/usuarios.component.ts` usa `https://my-cakephp-site.ddev.site/api/v1/users`. +- `src/app/usuarios/usuarios.component.ts` usa `https://my-cakephp-site.ddev.site/sse?debug=1`. + +Pendiente técnico: mover esas URLs a ruta relativa o configuración runtime, sin introducir secretos ni dominios PRO en el build. ## Namespace @@ -22,15 +99,21 @@ Puerto: `80` -## Validaciones +## Validaciones post-despliegue + +Estas validaciones aplican después de desplegar desde `infra-apps`, no desde el workflow de `appVestimenta`. ```powershell kubectl get pods -n gesplan-pre-vestimenta kubectl port-forward svc/vestimenta-frontend 8081:80 -n gesplan-pre-vestimenta curl.exe -I http://localhost:8081/ curl.exe -I http://localhost:8081/health -Pendiente -Integrar con backend mediante Ingress. -Activar OAuth2 Proxy específico de Vestimenta. -Crear Ingress definitivo. -Crear DNS funcional vestimenta-pre.gesplan.es. \ No newline at end of file +``` + +## Pendiente + +- Actualizar `image.tag` en `infra-apps` con el tag `sha-` generado. +- Integrar con backend mediante Ingress. +- Activar OAuth2 Proxy específico de Vestimenta. +- Crear Ingress definitivo. +- Crear DNS funcional `vestimenta-pre.gesplan.es`. diff --git a/src/app/app.component.spec.ts b/src/app/app.component.spec.ts index eef00cd..cc1d653 100644 --- a/src/app/app.component.spec.ts +++ b/src/app/app.component.spec.ts @@ -19,11 +19,4 @@ describe('AppComponent', () => { const app = fixture.componentInstance; expect(app.title).toEqual('angular19'); }); - - it('should render title', () => { - const fixture = TestBed.createComponent(AppComponent); - fixture.detectChanges(); - const compiled = fixture.nativeElement as HTMLElement; - expect(compiled.querySelector('h1')?.textContent).toContain('Hello, angular19'); - }); }); diff --git a/src/app/inicio/configurar/configurar.component.spec.ts b/src/app/inicio/configurar/configurar.component.spec.ts index ef0c147..c34de01 100644 --- a/src/app/inicio/configurar/configurar.component.spec.ts +++ b/src/app/inicio/configurar/configurar.component.spec.ts @@ -1,4 +1,6 @@ import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { provideHttpClient } from '@angular/common/http'; +import { provideHttpClientTesting } from '@angular/common/http/testing'; import { ConfigurarComponent } from './configurar.component'; @@ -8,7 +10,11 @@ describe('ConfigurarComponent', () => { beforeEach(async () => { await TestBed.configureTestingModule({ - imports: [ConfigurarComponent] + imports: [ConfigurarComponent], + providers: [ + provideHttpClient(), + provideHttpClientTesting() + ] }) .compileComponents(); diff --git a/src/app/services/sse.service.ts.service.spec.ts b/src/app/services/sse.service.ts.service.spec.ts index 97e56f1..bf70399 100644 --- a/src/app/services/sse.service.ts.service.spec.ts +++ b/src/app/services/sse.service.ts.service.spec.ts @@ -1,13 +1,13 @@ import { TestBed } from '@angular/core/testing'; -import { SseServiceTsService } from './sse.service.ts.service'; +import { SseService } from './sse.service.ts.service'; -describe('SseServiceTsService', () => { - let service: SseServiceTsService; +describe('SseService', () => { + let service: SseService; beforeEach(() => { TestBed.configureTestingModule({}); - service = TestBed.inject(SseServiceTsService); + service = TestBed.inject(SseService); }); it('should be created', () => { diff --git a/src/app/usuarios/usuarios.component.spec.ts b/src/app/usuarios/usuarios.component.spec.ts index ce16c9d..b36d62a 100644 --- a/src/app/usuarios/usuarios.component.spec.ts +++ b/src/app/usuarios/usuarios.component.spec.ts @@ -1,4 +1,6 @@ import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { provideHttpClient } from '@angular/common/http'; +import { provideHttpClientTesting } from '@angular/common/http/testing'; import { UsuariosComponent } from './usuarios.component'; @@ -8,7 +10,11 @@ describe('UsuariosComponent', () => { beforeEach(async () => { await TestBed.configureTestingModule({ - imports: [UsuariosComponent] + imports: [UsuariosComponent], + providers: [ + provideHttpClient(), + provideHttpClientTesting() + ] }) .compileComponents();