diff --git a/.github/workflows/cd-manual.yml b/.github/workflows/cd-manual.yml deleted file mode 100644 index df6aa628..00000000 --- a/.github/workflows/cd-manual.yml +++ /dev/null @@ -1,58 +0,0 @@ -name: Create docker images (manual) - -on: - workflow_dispatch: - inputs: - version: - type: string - description: Version - required: true - -jobs: - build: - name: Build, push, and deploy - runs-on: ubuntu-latest - - strategy: - matrix: - db-type: [postgresql] - - steps: - - uses: actions/checkout@v3 - - - name: Extract version parts from input - id: extract_version - run: | - echo "version=$(echo ${{ github.event.inputs.version }})" >> $GITHUB_ENV - echo "major=$(echo ${{ github.event.inputs.version }} | cut -d. -f1)" >> $GITHUB_ENV - echo "minor=$(echo ${{ github.event.inputs.version }} | cut -d. -f2)" >> $GITHUB_ENV - - - name: Generate tags - id: generate_tags - run: | - echo "tag_major=$(echo ${{ matrix.db-type }}-${{ env.major }})" >> $GITHUB_ENV - echo "tag_minor=$(echo ${{ matrix.db-type }}-${{ env.major }}.${{ env.minor }})" >> $GITHUB_ENV - echo "tag_patch=$(echo ${{ matrix.db-type }}-${{ env.version }})" >> $GITHUB_ENV - echo "tag_latest=$(echo ${{ matrix.db-type }}-latest)" >> $GITHUB_ENV - - - uses: mr-smithers-excellent/docker-build-push@v6 - name: Build & push Docker image to ghcr.io for ${{ matrix.db-type }} - with: - image: umami - tags: ${{ env.tag_major }}, ${{ env.tag_minor }}, ${{ env.tag_patch }}, ${{ env.tag_latest }} - buildArgs: DATABASE_TYPE=${{ matrix.db-type }} - registry: ghcr.io - multiPlatform: true - platform: linux/amd64,linux/arm64 - username: ${{ github.actor }} - password: ${{ secrets.GITHUB_TOKEN }} - - - uses: mr-smithers-excellent/docker-build-push@v6 - name: Build & push Docker image to docker.io for ${{ matrix.db-type }} - with: - image: umamisoftware/umami - tags: ${{ env.tag_major }}, ${{ env.tag_minor }}, ${{ env.tag_patch }}, ${{ env.tag_latest }} - buildArgs: DATABASE_TYPE=${{ matrix.db-type }} - registry: docker.io - username: ${{ secrets.DOCKER_USERNAME }} - password: ${{ secrets.DOCKER_PASSWORD }} diff --git a/.github/workflows/cd.yml b/.github/workflows/cd.yml index a02e9900..a9509bce 100644 --- a/.github/workflows/cd.yml +++ b/.github/workflows/cd.yml @@ -5,6 +5,11 @@ on: tags: - 'v*.*.*' workflow_dispatch: + inputs: + version: + description: 'Optional image version (e.g. 3.0.0, v3.0.0, or 3.0.0-beta.1)' + required: false + default: '' jobs: build: @@ -13,22 +18,20 @@ jobs: permissions: contents: read packages: write - id-token: write - - strategy: - matrix: - db-type: [postgresql] steps: - uses: actions/checkout@v5 - # Install cosign (for image signing) - - name: Install cosign - uses: sigstore/cosign-installer@v3 - - name: Set up Docker Buildx uses: docker/setup-buildx-action@v3 + - name: Log into GHCR + uses: docker/login-action@v3 + with: + registry: ghcr.io + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + - name: Log into Docker Hub if: github.repository == 'umami-software/umami' uses: docker/login-action@v3 @@ -37,44 +40,61 @@ jobs: username: ${{ secrets.DOCKER_USERNAME }} password: ${{ secrets.DOCKER_PASSWORD }} - - name: Log into GHCR - uses: docker/login-action@v3 - with: - registry: ghcr.io - username: ${{ github.actor }} - password: ${{ secrets.GITHUB_TOKEN }} + - name: Compute version tags + id: compute + run: | + INPUT="${{ github.event.inputs.version }}" + REF_TYPE="${{ github.ref_type }}" + REF_NAME="${{ github.ref_name }}" - - name: Extract Docker metadata - id: meta - uses: docker/metadata-action@v5 - with: - images: | - umamisoftware/umami,enable=${{ github.repository == 'umami-software/umami' }} - ghcr.io/${{ github.repository }} - flavor: | - latest=auto - prefix=${{ matrix.db-type }}- - tags: | - type=semver,pattern={{version}} - type=semver,pattern={{major}}.{{minor}} - type=semver,pattern={{major}} + # Determine version source + if [[ -n "$INPUT" ]]; then + VERSION="${INPUT#v}" + elif [[ "$REF_TYPE" == "tag" ]]; then + VERSION="${REF_NAME#v}" + else + VERSION="" + fi + + TAGS="" + + if [[ -n "$VERSION" ]]; then + MAJOR=$(echo "$VERSION" | cut -d. -f1) + MINOR=$(echo "$VERSION" | cut -d. -f2) + + if [[ "$VERSION" == *-* ]]; then + # prerelease: only version tag + TAGS="$VERSION" + else + # stable release: version + hierarchy + latest + TAGS="$VERSION,${MAJOR}.${MINOR},${MAJOR},postgresql-latest,latest" + fi + else + # Non-tag build (e.g. from main branch) + TAGS="${REF_NAME}" + fi + + echo "tags=$TAGS" >> $GITHUB_OUTPUT + echo "Computed tags: $TAGS" - name: Build and push Docker image - id: build-and-push - uses: docker/build-push-action@v6 - with: - context: . - platforms: linux/amd64,linux/arm64 - build-args: DATABASE_TYPE=${{ matrix.db-type }} - push: true - tags: ${{ steps.meta.outputs.tags }} - labels: ${{ steps.meta.outputs.labels }} - cache-from: type=gha - cache-to: type=gha,mode=max + run: | + TAGS="${{ steps.compute.outputs.tags }}" - # Sign the published image digest - - name: Sign the published Docker image - env: - TAGS: ${{ steps.meta.outputs.tags }} - DIGEST: ${{ steps.build-and-push.outputs.digest }} - run: echo "${TAGS}" | xargs -I {} cosign sign --yes "{}@${DIGEST}" + # Set image targets conditionally + if [[ "${{ github.repository }}" == "umami-software/umami" ]]; then + IMAGES=("umamisoftware/umami" "ghcr.io/${{ github.repository }}") + else + IMAGES=("ghcr.io/${{ github.repository }}") + fi + + for IMAGE in "${IMAGES[@]}"; do + echo "Building and pushing $IMAGE with tags: $TAGS" + docker buildx build \ + --platform linux/amd64,linux/arm64 \ + --push \ + $(echo "$TAGS" | tr ',' '\n' | sed "s|^|--tag ${IMAGE}:|") \ + --cache-from type=gha \ + --cache-to type=gha,mode=max \ + . + done diff --git a/.github/workflows/delete-untagged-images.yml b/.github/workflows/delete-untagged-images.yml deleted file mode 100644 index a23a1bd2..00000000 --- a/.github/workflows/delete-untagged-images.yml +++ /dev/null @@ -1,22 +0,0 @@ -name: Delete untagged GHCR images - -on: - workflow_dispatch: # Run manually from the Actions tab - -jobs: - cleanup: - name: Delete all untagged images - runs-on: ubuntu-latest - - permissions: - packages: write - contents: read - - steps: - - name: Delete untagged GHCR images - uses: actions/delete-package-versions@v5 - with: - package-name: "umami" # 👈 change if your GHCR package name differs - package-type: "container" - delete-only-untagged-versions: true - min-versions-to-keep: 0 diff --git a/README.md b/README.md index 6d166d8c..d3791e26 100644 --- a/README.md +++ b/README.md @@ -89,7 +89,7 @@ docker compose up -d Alternatively, to pull just the Umami Docker image with PostgreSQL support: ```bash -docker pull docker.umami.is/umami-software/umami:postgresql-latest +docker pull docker.umami.is/umami-software/umami:latest ``` --- diff --git a/docker-compose.yml b/docker-compose.yml index 7b51db66..8c8a47a6 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,7 +1,7 @@ --- services: umami: - image: ghcr.io/umami-software/umami:postgresql-latest + image: ghcr.io/umami-software/umami:latest ports: - "3000:3000" environment: diff --git a/src/app/(main)/App.tsx b/src/app/(main)/App.tsx index 32218d11..7700639d 100644 --- a/src/app/(main)/App.tsx +++ b/src/app/(main)/App.tsx @@ -9,18 +9,14 @@ import { MobileNav } from '@/app/(main)/MobileNav'; export function App({ children }) { const { user, isLoading, error } = useLoginQuery(); const config = useConfig(); - const { pathname, router } = useNavigation(); + const { pathname } = useNavigation(); if (isLoading || !config) { return ; } if (error) { - if (process.env.cloudMode) { - window.location.href = '/login'; - } else { - router.push('/login'); - } + window.location.href = '/login'; return null; } diff --git a/src/app/logout/LogoutPage.tsx b/src/app/logout/LogoutPage.tsx index 909f35de..bd471796 100644 --- a/src/app/logout/LogoutPage.tsx +++ b/src/app/logout/LogoutPage.tsx @@ -13,7 +13,7 @@ export function LogoutPage() { async function logout() { await post('/auth/logout'); - router.push('/login'); + window.location.href = '/login'; } removeClientAuthToken(); diff --git a/src/lib/__tests__/detect.test.ts b/src/lib/__tests__/detect.test.ts index fcf706af..f02ac839 100644 --- a/src/lib/__tests__/detect.test.ts +++ b/src/lib/__tests__/detect.test.ts @@ -1,4 +1,4 @@ -import * as detect from '../detect'; +import { getIpAddress } from '../ip'; const IP = '127.0.0.1'; const BAD_IP = '127.127.127.127'; @@ -6,23 +6,23 @@ const BAD_IP = '127.127.127.127'; test('getIpAddress: Custom header', () => { process.env.CLIENT_IP_HEADER = 'x-custom-ip-header'; - expect(detect.getIpAddress(new Headers({ 'x-custom-ip-header': IP }))).toEqual(IP); + expect(getIpAddress(new Headers({ 'x-custom-ip-header': IP }))).toEqual(IP); }); test('getIpAddress: CloudFlare header', () => { - expect(detect.getIpAddress(new Headers({ 'cf-connecting-ip': IP }))).toEqual(IP); + expect(getIpAddress(new Headers({ 'cf-connecting-ip': IP }))).toEqual(IP); }); test('getIpAddress: Standard header', () => { - expect(detect.getIpAddress(new Headers({ 'x-forwarded-for': IP }))).toEqual(IP); + expect(getIpAddress(new Headers({ 'x-forwarded-for': IP }))).toEqual(IP); }); test('getIpAddress: CloudFlare header is lower priority than standard header', () => { - expect( - detect.getIpAddress(new Headers({ 'cf-connecting-ip': BAD_IP, 'x-forwarded-for': IP })), - ).toEqual(IP); + expect(getIpAddress(new Headers({ 'cf-connecting-ip': BAD_IP, 'x-forwarded-for': IP }))).toEqual( + IP, + ); }); test('getIpAddress: No header', () => { - expect(detect.getIpAddress(new Headers())).toEqual(null); + expect(getIpAddress(new Headers())).toEqual(null); });