name: frontend-ci on: push: branches: ["**"] pull_request: jobs: lint: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - uses: actions/setup-node@v4 with: node-version: 22 cache: npm cache-dependency-path: package-lock.json - run: npm ci - run: npx eslint . typecheck: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - uses: actions/setup-node@v4 with: node-version: 22 cache: npm cache-dependency-path: package-lock.json - run: npm ci - run: npx tsc -b secrets-scan: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 with: fetch-depth: 0 - name: Install and run gitleaks run: | GL_VERSION=8.18.4 curl -sSL "https://github.com/gitleaks/gitleaks/releases/download/v${GL_VERSION}/gitleaks_${GL_VERSION}_linux_x64.tar.gz" \ | tar xz -C /tmp gitleaks /tmp/gitleaks detect --redact --no-banner --verbose --source . sast: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - uses: returntocorp/semgrep-action@v1 with: config: "p/auto" fs-scan: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - name: Install and run Trivy (filesystem) run: | TRIVY_VERSION=0.70.0 curl -sSL "https://github.com/aquasecurity/trivy/releases/download/v${TRIVY_VERSION}/trivy_${TRIVY_VERSION}_Linux-64bit.tar.gz" \ | tar xz -C /tmp trivy /tmp/trivy fs --severity HIGH,CRITICAL --exit-code 1 --ignore-unfixed --no-progress . build: runs-on: ubuntu-latest needs: [lint, typecheck] steps: - uses: actions/checkout@v4 - uses: docker/setup-buildx-action@v3 - uses: docker/build-push-action@v5 with: context: . file: Dockerfile target: production tags: movieloop-frontend:ci-${{ github.sha }} load: true - name: Install and run Trivy (image) run: | TRIVY_VERSION=0.70.0 curl -sSL "https://github.com/aquasecurity/trivy/releases/download/v${TRIVY_VERSION}/trivy_${TRIVY_VERSION}_Linux-64bit.tar.gz" \ | tar xz -C /tmp trivy /tmp/trivy image --severity HIGH,CRITICAL --exit-code 1 --ignore-unfixed --no-progress \ movieloop-frontend:ci-${{ github.sha }} push: runs-on: ubuntu-latest needs: [build, secrets-scan, sast, fs-scan] if: github.event_name == 'push' && github.ref == 'refs/heads/main' env: HARBOR_HOST: ${{ secrets.HARBOR_HOST }} HARBOR_PROJECT: movieloop IMAGE_NAME: frontend steps: - uses: actions/checkout@v4 with: # Need full history so the back-pushed git tag can be created against # the right commit, and so token-auth on push to origin works. fetch-depth: 0 - uses: docker/setup-buildx-action@v3 - name: Compute tag inputs run: | SHA_SHORT=$(git rev-parse --short HEAD) VERSION=$(jq -r .version package.json) echo "SHA_SHORT=${SHA_SHORT}" >> "$GITHUB_ENV" echo "VERSION=${VERSION}" >> "$GITHUB_ENV" - name: Refuse to overwrite an existing version tag in Harbor env: HARBOR_USERNAME: ${{ secrets.MOVIELOOP_USERNAME }} HARBOR_PASSWORD: ${{ secrets.MOVIELOOP_PASSWORD }} run: | set -eu url="https://${HARBOR_HOST}/api/v2.0/projects/${HARBOR_PROJECT}/repositories/${IMAGE_NAME}/artifacts/${VERSION}/tags" code=$(curl -s -o /dev/null -w "%{http_code}" -u "${HARBOR_USERNAME}:${HARBOR_PASSWORD}" "${url}") if [ "$code" = "200" ]; then echo "::error::Tag ${HARBOR_PROJECT}/${IMAGE_NAME}:${VERSION} already exists in Harbor. Bump package.json before merging." exit 1 fi if [ "$code" != "404" ]; then echo "::warning::Unexpected status ${code} checking ${url} — proceeding." fi - name: Log in to Harbor env: HARBOR_USERNAME: ${{ secrets.MOVIELOOP_USERNAME }} HARBOR_PASSWORD: ${{ secrets.MOVIELOOP_PASSWORD }} run: | echo "${HARBOR_PASSWORD}" | docker login "${HARBOR_HOST}" -u "${HARBOR_USERNAME}" --password-stdin - name: Build, tag, and push image run: | set -eu REPO="${HARBOR_HOST}/${HARBOR_PROJECT}/${IMAGE_NAME}" docker buildx build --load --target production -t movieloop-${IMAGE_NAME}:${SHA_SHORT} -f Dockerfile . docker tag movieloop-${IMAGE_NAME}:${SHA_SHORT} ${REPO}:${VERSION} docker tag movieloop-${IMAGE_NAME}:${SHA_SHORT} ${REPO}:${SHA_SHORT} docker tag movieloop-${IMAGE_NAME}:${SHA_SHORT} ${REPO}:latest docker push ${REPO}:${VERSION} docker push ${REPO}:${SHA_SHORT} docker push ${REPO}:latest - name: Install cosign run: | curl -fsSL "https://github.com/sigstore/cosign/releases/download/v2.4.1/cosign-linux-amd64" \ -o /usr/local/bin/cosign chmod +x /usr/local/bin/cosign cosign version - name: Sign image with cosign env: COSIGN_KEY: ${{ secrets.COSIGN_PRIVATE_KEY }} COSIGN_PASSWORD: ${{ secrets.COSIGN_PASSWORD }} run: | set -eu # Sign by SHA tag — cosign resolves to the underlying digest, so a # single signature covers every tag pointing at the same digest # (version, sha, latest). Harbor looks up signatures by digest. # Reuses the docker login from earlier in this job for registry auth. cosign sign --key env://COSIGN_KEY --yes \ "${HARBOR_HOST}/${HARBOR_PROJECT}/${IMAGE_NAME}:${SHA_SHORT}" - name: Push back git tag env: # Auto-injected per-run token; has push permission to this repo. GITEA_TOKEN: ${{ secrets.GITHUB_TOKEN }} run: | set -eu git config user.name "gitea-actions[bot]" git config user.email "gitea-actions[bot]@tehriehldeal.com" git tag "v${VERSION}" -m "${IMAGE_NAME} v${VERSION}" 2>/dev/null || true ORIGIN="$(git remote get-url origin | sed -E "s#https?://([^/]+)#https://${GITEA_TOKEN}@\1#")" # A failed tag push shouldn't undo a successful image push — log # and proceed so we don't poison subsequent retries. git push "$ORIGIN" --tags || echo "::warning::Tag push to origin failed — image is pushed; create the tag manually if needed." - name: Log out of Harbor if: always() run: docker logout "${HARBOR_HOST}" || true