d36121e673
frontend-ci / secrets-scan (push) Successful in 6s
frontend-ci / fs-scan (push) Successful in 11s
frontend-ci / typecheck (push) Successful in 13s
frontend-ci / lint (push) Successful in 16s
frontend-ci / sast (push) Successful in 18s
frontend-ci / build (push) Successful in 38s
frontend-ci / push (push) Has been skipped
Mirrors the backend push-job rewrite:
- Replace docker/login-action@v3 (fast 1s failure on the Gitea
runner) with plain `docker login --password-stdin`.
- Read VERSION from package.json (jq), SHA_SHORT from git, publish
:VERSION, :SHA_SHORT, :latest. Drop floating :1.2, :1 tags.
- Pre-check Harbor for an existing :VERSION artifact and fail early
with a "bump package.json" message instead of overwriting silently.
- Sign each pushed image with cosign. Signature is on the digest
(resolved via :SHA_SHORT), so it covers all tags pointing at it.
- After a successful image push, create and push a `v${VERSION}` git
tag back to origin using the auto-injected token. Tag-push failure
is a warning, not an error.
- Drop `tags: ["v*"]` from the workflow trigger — CI now creates the
tag itself, so re-triggering on tag push would loop.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
181 lines
6.6 KiB
YAML
181 lines
6.6 KiB
YAML
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
|