diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml new file mode 100644 index 0000000000..ec24ae0c33 --- /dev/null +++ b/.github/workflows/main.yml @@ -0,0 +1,288 @@ +name: Main + +on: + push: + branches: ["main"] + tags: + - "*-?v[0-9]+*" + pull_request: + branches: ["main"] + types: [opened, synchronize] + +env: + GHCR_REGISTRY: ghcr.io + +permissions: + contents: write + packages: write + +jobs: + pre-workflow-checks: + runs-on: ubuntu-latest + outputs: + should-run: ${{ steps.check.outputs.should-run }} + should-release: ${{ steps.tag.outputs.should-release }} + + steps: + - uses: actions/checkout@v4 + with: + fetch-depth: 2 + - name: Check commit message + id: check + run: | + if [[ "${{ github.event_name }}" == "push" ]]; then + echo "should-run=true" >> $GITHUB_OUTPUT + elif [[ "${{ github.event_name }}" == "pull_request" ]]; then + COMMIT_MSG=$(git log --format=%B ${{ github.event.pull_request.head.sha }}) + if [[ "$COMMIT_MSG" == *"Run CI"* ]]; then + echo "should-run=true" >> $GITHUB_OUTPUT + else + echo "should-run=false" >> $GITHUB_OUTPUT + fi + else + echo "should-run=false" >> $GITHUB_OUTPUT + fi + - name: Check tag + id: tag + run: | + if [[ "${{ github.event_name }}" == "push" && "${{ github.ref }}" == "refs/tags/${{ github.ref_name }}" ]]; then + echo "should-release=true" >> $GITHUB_OUTPUT + else + echo "should-release=false" >> $GITHUB_OUTPUT + fi + + create-release: + needs: + - pre-workflow-checks + if: needs.pre-workflow-checks.outputs.should-release == 'true' + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v4 + with: + fetch-depth: 0 + - name: Generate changelog + id: changelog + uses: metcalfc/changelog-generator@v4.1.0 + with: + myToken: ${{ secrets.GITHUB_TOKEN }} + - name: Create Release + uses: softprops/action-gh-release@v1 + with: + body: | + ## What's Changed + ${{ steps.changelog.outputs.changelog }} + draft: false + prerelease: false + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + + build-transactional: + needs: pre-workflow-checks + if: needs.pre-workflow-checks.outputs.should-run == 'true' + runs-on: ubuntu-latest + env: + MOON_TOOLCHAIN_FORCE_GLOBALS: true + + steps: + - uses: actions/checkout@v4 + - uses: moonrepo/setup-toolchain@v0 + with: + auto-install: true + - name: Set up Node.js and caching + uses: actions/setup-node@v3 + with: + node-version: '20' + cache: 'yarn' + - name: Build emails + run: moon run transactional:build + - name: Upload templates artifact + uses: actions/upload-artifact@v4 + with: + name: templates + path: | + apps/backend/templates/ + retention-days: 1 + + build-backend: + needs: + - pre-workflow-checks + - build-transactional + if: needs.pre-workflow-checks.outputs.should-run == 'true' + runs-on: ubuntu-latest + + strategy: + fail-fast: false + matrix: + platform: + - target: x86_64-unknown-linux-gnu + command: cargo + - target: aarch64-unknown-linux-gnu + command: cross + + steps: + - uses: actions/checkout@v4 + - uses: actions/download-artifact@v4 + with: + name: templates + path: ${{ github.workspace }}/apps/backend/templates/ + - name: Extract build information + id: build + env: + TARGET: ${{ matrix.platform.target }} + run: | + echo "version=${GITHUB_REF##*/}" >> "$GITHUB_OUTPUT" + echo "docker-arch=${{ startsWith(matrix.platform.target, 'x86_64') && 'amd64' || 'arm64' }}" >> "$GITHUB_OUTPUT" + - name: Extract rust toolchain + id: toolchain + run: | + echo "channel=$(grep channel rust-toolchain.toml | awk -F' = ' '{printf $2}' | tr -d '\"')" >> "$GITHUB_OUTPUT" + - uses: dtolnay/rust-toolchain@master + with: + toolchain: ${{ steps.toolchain.outputs.channel }} + targets: ${{ matrix.platform.target }} + - uses: Swatinem/rust-cache@v2 + with: + key: ${{ matrix.platform.target }}-${{ steps.build.outputs.profile }} + save-if: ${{ github.event_name != 'pull_request' }} + - name: Install cross + if: ${{ matrix.platform.command == 'cross' }} + uses: taiki-e/cache-cargo-install-action@v2 + with: + tool: cross + git: https://github.com/cross-rs/cross + rev: 19be83481fd3e50ea103d800d72e0f8eddb1c90c + locked: false + - name: Build + env: + APP_VERSION: ${{ steps.build.outputs.version }} + DEFAULT_TMDB_ACCESS_TOKEN: ${{ secrets.DEFAULT_TMDB_ACCESS_TOKEN }} + DEFAULT_MAL_CLIENT_ID: ${{ secrets.DEFAULT_MAL_CLIENT_ID }} + DEFAULT_GOOGLE_BOOKS_API_KEY: ${{ secrets.DEFAULT_GOOGLE_BOOKS_API_KEY }} + run: | + ${{ matrix.platform.command }} build --locked --target ${{ matrix.platform.target }} --release + - name: Upload build artifact + uses: actions/upload-artifact@v4 + with: + name: backend-${{ steps.build.outputs.docker-arch }} + path: ${{ github.workspace }}/target/${{ matrix.platform.target }}/release/ryot + retention-days: 1 + + build-docker: + needs: + - pre-workflow-checks + - build-backend + if: needs.pre-workflow-checks.outputs.should-run == 'true' + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v4 + - name: Download build artifact for docker + uses: actions/download-artifact@v4 + with: + path: ${{ github.workspace }}/artifact/ + - name: Set up QEMU + uses: docker/setup-qemu-action@v3 + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 + - name: Log in to the ghcr container registry + uses: docker/login-action@v3 + with: + registry: ${{ env.GHCR_REGISTRY }} + username: ${{ github.repository_owner }} + password: ${{ secrets.GITHUB_TOKEN }} + - name: Log in to the docker hub container registry + uses: docker/login-action@v3 + with: + username: ${{ secrets.DOCKER_USERNAME }} + password: ${{ secrets.DOCKER_TOKEN }} + - name: Extract metadata for Docker + id: meta + uses: docker/metadata-action@v5 + with: + images: | + name=${{ secrets.DOCKER_USERNAME }}/${{ github.event.repository.name }},enable={{is_default_branch}} + name=${{ env.GHCR_REGISTRY }}/${{ github.repository_owner }}/${{ github.event.repository.name }} + tags: | + type=ref,event=pr + type=semver,pattern=v{{version}},enable={{is_default_branch}} + type=semver,pattern=v{{major}}.{{minor}},enable={{is_default_branch}} + type=semver,pattern=v{{major}},enable={{is_default_branch}} + type=raw,value=develop,enable={{is_default_branch}} + - name: Build and push + uses: docker/build-push-action@v5 + with: + context: . + push: true + cache-from: type=gha + cache-to: type=gha,mode=max + platforms: linux/amd64,linux/arm64 + tags: ${{ steps.meta.outputs.tags }} + + upload-kodi-plugin: + needs: + - pre-workflow-checks + - build-docker + if: needs.pre-workflow-checks.outputs.should-release == 'true' + runs-on: ubuntu-20.04 + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + MOON_TOOLCHAIN_FORCE_GLOBALS: true + + steps: + - uses: actions/checkout@v4 + - name: Setup Moon + uses: moonrepo/setup-toolchain@v0 + with: + auto-install: true + - name: Set up Node.js and caching + uses: actions/setup-node@v3 + with: + node-version: '20' + cache: 'yarn' + - name: Build plugin + run: moon run kodi:build + - name: Upload plugin to releases + run: gh release upload --clobber ${{ github.ref_name }} "tmp/script.ryot.zip" + + deploy-demo-instance: + needs: + - pre-workflow-checks + - build-docker + if: needs.pre-workflow-checks.outputs.should-release == 'true' + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v4 + - name: Set up CLI + uses: superfly/flyctl-actions/setup-flyctl@master + - name: Deploy + run: flyctl deploy --remote-only --detach --config ci/fly.toml + env: + FLY_API_TOKEN: ${{ secrets.FLY_API_TOKEN }} + + deploy-docs: + needs: + - pre-workflow-checks + - build-docker + if: needs.pre-workflow-checks.outputs.should-release == 'true' + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-python@v5 + with: + python-version: 3.x + - name: Install poetry + uses: abatilo/actions-poetry@v2 + - name: Install dependencies + run: cd docs && poetry install + - name: Build docs + run: cd docs && poetry run mkdocs build + - name: Push to deployment branch + uses: peaceiris/actions-gh-pages@v4 + with: + github_token: ${{ secrets.GITHUB_TOKEN }} + publish_dir: docs/site + publish_branch: nf-docs + force_orphan: true diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml deleted file mode 100644 index 6abdd27791..0000000000 --- a/.github/workflows/release.yml +++ /dev/null @@ -1,195 +0,0 @@ -name: Release - -on: - push: - tags: - - "*-?v[0-9]+*" - -env: - GHCR_REGISTRY: ghcr.io - -permissions: - contents: write - packages: write - -jobs: - create-release: - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v4 - - - name: Create or update release - uses: actions/github-script@v7 - env: - TAG_NAME: ${{ github.ref_name }} - with: - script: | - const tag = process.env.TAG_NAME || github.ref_name; - const repo = context.repo; - const majorVersion = tag.match(/v(\d+)\.\d+\.\d+/)[1]; - const releaseName = `Version ${majorVersion}`; - const date = new Date().toLocaleDateString('en-US', { year: 'numeric', month: 'long', day: 'numeric' }); - const dynamicBody = `\n*Created from tag*: \`${tag}\`\n*Created from git revision*: \`${context.sha}\`\n*Published on*: \`${date}\`\n`; - - async function findOrCreateRelease() { - const { data: releases } = await github.rest.repos.listReleases(repo); - let existingRelease = releases.find(release => release.name === releaseName); - - if (existingRelease) { - const existingBody = existingRelease.body; - const newBody = existingBody.replace(/[^]*/, dynamicBody); - await github.rest.repos.updateRelease({ - ...repo, - release_id: existingRelease.id, - tag_name: tag, - body: newBody, - draft: false, - }); - console.log("Release updated to associate with new tag."); - } else { - const fullBody = `${dynamicBody}\n\n## Release Notes\n\n- Some bug fixes.`; - await github.rest.repos.createRelease({ - ...repo, - tag_name: tag, - name: releaseName, - body: fullBody, - draft: false, - prerelease: false - }); - console.log("Release created successfully."); - } - } - - findOrCreateRelease(); - - docker-release: - runs-on: ubuntu-latest - needs: create-release - steps: - - uses: actions/checkout@v4 - - - name: Set up QEMU - uses: docker/setup-qemu-action@v3 - - - name: Set up Docker Buildx - uses: docker/setup-buildx-action@v3 - - - name: Log in to the ghcr container registry - uses: docker/login-action@v3 - with: - registry: ${{ env.GHCR_REGISTRY }} - username: ${{ github.actor }} - password: ${{ secrets.GITHUB_TOKEN }} - - - name: Log in to the docker hub container registry - uses: docker/login-action@v3 - with: - username: ${{ secrets.DOCKER_USERNAME }} - password: ${{ secrets.DOCKER_TOKEN }} - - - name: Get docker image names - id: required_args - uses: actions/github-script@v7 - env: - DOCKER_USERNAME: ${{ secrets.DOCKER_USERNAME }} - with: - script: | - const repoName = context.payload.repository.name; - const refName = context.ref.replace('refs/tags/', ''); - core.setOutput('APP_VERSION', refName); - - const dockerHubActor = process.env.DOCKER_USERNAME; - const ghcrRegistry = process.env.GHCR_REGISTRY; - const ghcrActor = context.actor; - - function generateVersionArray(version) { - const parts = version.split("."); - const versionArray = []; - for (let i = 0; i < parts.length; i++) - versionArray.push(parts.slice(0, i + 1).join(".")); - versionArray.push("latest"); - return versionArray; - } - - const versionTags = generateVersionArray(refName); - const ghcrImageName = `${ghcrRegistry}/${ghcrActor}/${repoName}`; - const ghcrTags = versionTags.map((tag) => `${ghcrImageName}:${tag}`); - - const dockerHubImageName = `${dockerHubActor}/${repoName}`; - const dockerHubTags = versionTags.map((tag) => `${dockerHubImageName}:${tag}`); - - const imageNames = [...ghcrTags, dockerHubTags].join(",").toLowerCase(); - core.setOutput('image_names', imageNames); - - - name: Build and push to ghcr - uses: docker/build-push-action@v5 - with: - context: . - platforms: linux/amd64,linux/arm64 - push: true - tags: ${{ steps.required_args.outputs.image_names }} - build-args: | - APP_VERSION=${{ steps.required_args.outputs.APP_VERSION }} - DEFAULT_TMDB_ACCESS_TOKEN=${{ secrets.DEFAULT_TMDB_ACCESS_TOKEN }} - DEFAULT_MAL_CLIENT_ID=${{ secrets.DEFAULT_MAL_CLIENT_ID }} - DEFAULT_GOOGLE_BOOKS_API_KEY=${{ secrets.DEFAULT_GOOGLE_BOOKS_API_KEY }} - - upload-kodi-plugin: - runs-on: ubuntu-20.04 - needs: docker-release - env: - GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} - steps: - - uses: actions/checkout@v4 - - - name: Setup Moon - uses: moonrepo/setup-toolchain@v0 - with: - auto-install: true - - - name: Build plugin - run: moon run kodi:build - - - name: Upload plugin to releases - run: gh release upload --clobber ${{ github.ref_name }} "tmp/script.ryot.zip" - - deploy-demo-instance: - runs-on: ubuntu-latest - needs: docker-release - steps: - - uses: actions/checkout@v4 - - - name: Set up CLI - uses: superfly/flyctl-actions/setup-flyctl@master - - - name: Deploy - run: flyctl deploy --remote-only --detach --config ci/fly.toml - env: - FLY_API_TOKEN: ${{ secrets.FLY_API_TOKEN }} - - deploy-docs: - runs-on: ubuntu-latest - needs: docker-release - steps: - - uses: actions/checkout@v4 - - - uses: actions/setup-python@v5 - with: - python-version: 3.x - - - name: Install poetry - uses: abatilo/actions-poetry@v2 - - - name: Install dependencies - run: cd docs && poetry install - - - name: Build docs - run: cd docs && poetry run mkdocs build - - - name: Push to deployment branch - uses: peaceiris/actions-gh-pages@v4 - with: - github_token: ${{ secrets.GITHUB_TOKEN }} - publish_dir: docs/site - publish_branch: nf-docs - force_orphan: true diff --git a/Cross.toml b/Cross.toml new file mode 100644 index 0000000000..d1136ace5b --- /dev/null +++ b/Cross.toml @@ -0,0 +1,10 @@ +[build.env] +passthrough = [ + "APP_VERSION", + "DEFAULT_TMDB_ACCESS_TOKEN", + "DEFAULT_MAL_CLIENT_ID", + "DEFAULT_GOOGLE_BOOKS_API_KEY", +] + +[build] +pre-build = "ci/cross-pre-build.sh" diff --git a/Dockerfile b/Dockerfile index 679f77e850..03d71c1196 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,8 +1,6 @@ ARG NODE_BASE_IMAGE=node:20.10.0-bookworm-slim -FROM --platform=$BUILDPLATFORM $NODE_BASE_IMAGE AS node-build-base - -FROM node-build-base AS frontend-build-base +FROM $NODE_BASE_IMAGE AS frontend-build-base ENV MOON_TOOLCHAIN_FORCE_GLOBALS=true WORKDIR /app RUN apt update && apt install -y --no-install-recommends git curl ca-certificates xz-utils @@ -11,63 +9,41 @@ RUN npm install -g @moonrepo/cli && moon --version FROM frontend-build-base AS frontend-workspace WORKDIR /app COPY . . -RUN moon docker scaffold frontend transactional +RUN moon docker scaffold frontend FROM frontend-build-base AS frontend-builder WORKDIR /app COPY --from=frontend-workspace /app/.moon/docker/workspace . RUN moon docker setup COPY --from=frontend-workspace /app/.moon/docker/sources . -RUN moon run frontend:build transactional:build +RUN moon run frontend:build RUN moon docker prune -FROM --platform=$BUILDPLATFORM lukemathwalker/cargo-chef AS backend-chef -RUN apt-get update && apt-get install -y --no-install-recommends gcc-aarch64-linux-gnu libc6-dev-arm64-cross clang llvm ca-certificates pkg-config make g++ libssl-dev -RUN update-ca-certificates -WORKDIR app - -FROM backend-chef AS backend-planner -COPY . . -RUN cargo chef prepare --recipe-path recipe.json - -FROM backend-chef AS backend-builder -# build specific -ARG BUILD_PROFILE=release -# application specific -ARG APP_VERSION -ARG DEFAULT_TMDB_ACCESS_TOKEN -ARG DEFAULT_MAL_CLIENT_ID -ARG DEFAULT_GOOGLE_BOOKS_API_KEY -RUN test -n "$APP_VERSION" && \ - test -n "$DEFAULT_TMDB_ACCESS_TOKEN" && \ - test -n "$DEFAULT_MAL_CLIENT_ID" && \ - test -n "$DEFAULT_GOOGLE_BOOKS_API_KEY" -COPY --from=backend-planner /app/recipe.json recipe.json -RUN cargo chef cook --profile $BUILD_PROFILE --recipe-path recipe.json -COPY . . -COPY --from=frontend-builder /app/apps/backend/templates ./apps/backend/templates -RUN APP_VERSION=$APP_VERSION \ - DEFAULT_TMDB_ACCESS_TOKEN=$DEFAULT_TMDB_ACCESS_TOKEN \ - DEFAULT_MAL_CLIENT_ID=$DEFAULT_MAL_CLIENT_ID \ - DEFAULT_GOOGLE_BOOKS_API_KEY=$DEFAULT_GOOGLE_BOOKS_API_KEY \ - cargo build --profile ${BUILD_PROFILE} --bin ryot -RUN cp -R /app/target/${BUILD_PROFILE}/ryot /app/ryot +FROM --platform=${BUILDPLATFORM} alpine as artifact +COPY artifact/ /artifact/ +ARG TARGETARCH +ENV TARGETARCH=${TARGETARCH} +RUN mv /artifact/backend-${TARGETARCH}/ryot /artifact/ryot +RUN chmod +x /artifact/ryot FROM $NODE_BASE_IMAGE +ARG TARGETARCH +ENV TARGETARCH=${TARGETARCH} LABEL org.opencontainers.image.source="https://github.com/IgnisDa/ryot" ENV FRONTEND_UMAMI_SCRIPT_URL="https://umami.diptesh.me/script.js" ENV FRONTEND_UMAMI_WEBSITE_ID="5ecd6915-d542-4fda-aa5f-70f09f04e2e0" COPY --from=caddy:2.7.5 /usr/bin/caddy /usr/local/bin/caddy -RUN apt-get update && apt-get install -y --no-install-recommends curl ca-certificates procps && rm -rf /var/lib/apt/lists/* +RUN apt-get update && apt-get install -y --no-install-recommends libssl3 ca-certificates procps && rm -rf /var/lib/apt/lists/* RUN npm install --global concurrently@8.2.2 && concurrently --version RUN useradd -m -u 1001 ryot +RUN if [ "${TARGETARCH}" = "arm64" ]; then apt-get update && apt-get install -y --no-install-recommends wget && wget http://ftp.debian.org/debian/pool/main/o/openssl/libssl1.1_1.1.1w-0+deb11u1_arm64.deb && dpkg -i libssl1.1_1.1.1w-0+deb11u1_arm64.deb && rm -rf libssl1.1_1.1.1w-0+deb11u1_arm64.deb && apt-get remove -y wget && rm -rf rm -rf /var/lib/apt/lists/*; fi WORKDIR /home/ryot USER ryot COPY ci/Caddyfile /etc/caddy/Caddyfile COPY --from=frontend-builder --chown=ryot:ryot /app/apps/frontend/node_modules ./node_modules COPY --from=frontend-builder --chown=ryot:ryot /app/apps/frontend/package.json ./package.json COPY --from=frontend-builder --chown=ryot:ryot /app/apps/frontend/build ./build -COPY --from=backend-builder --chown=ryot:ryot /app/ryot /usr/local/bin/ryot +COPY --from=artifact --chown=ryot:ryot /artifact/ryot /usr/local/bin/ryot CMD [ \ "concurrently", "--names", "frontend,backend,proxy", "--kill-others", \ "PORT=3000 npx remix-serve ./build/server/index.js", \ diff --git a/ci/cross-pre-build.sh b/ci/cross-pre-build.sh new file mode 100644 index 0000000000..3b15d0e343 --- /dev/null +++ b/ci/cross-pre-build.sh @@ -0,0 +1,7 @@ +#!/usr/bin/env bash +set -euxo pipefail + +if [[ "$CROSS_TARGET" == "aarch64-unknown-linux-gnu" ]]; then + dpkg --add-architecture $CROSS_DEB_ARCH + apt-get update && apt-get --assume-yes install libssl-dev:$CROSS_DEB_ARCH +fi