From 3310b61a8f5c6f3473a9fb630f9b4fe86ecf3a8e Mon Sep 17 00:00:00 2001 From: Ivan Date: Thu, 23 Apr 2026 19:49:05 -0500 Subject: [PATCH] chore(workflows): migrate CI workflows from Gitea to GitHub Actions and remove obsolete files --- .gitea/workflows/bench.yml | 58 ----- .gitea/workflows/build-test.yml | 84 ------- .gitea/workflows/build.yml | 195 --------------- .gitea/workflows/ci.yml | 176 ------------- .gitea/workflows/docker.yml | 102 -------- .gitea/workflows/github-release-sync.yml | 70 ++++++ .gitea/workflows/rekor-monitor.yml | 37 --- .gitea/workflows/scan.yml | 51 ---- .gitea/workflows/tests.yml | 66 ----- .github/workflows/bench.yml | 63 +++++ .github/workflows/build-linux-release.yml | 190 ++++++++++++++ .github/workflows/build-release.yml | 76 +++++- scripts/ci/docker-build-entry.sh | 71 ++++++ scripts/ci/github-apt-linux-packaging.sh | 20 ++ .../ci/github-build-linux-release-assets.sh | 60 +++++ .../ci/github-draft-release-upload-assets.sh | 31 +++ scripts/ci/github-install-deps.sh | 7 +- scripts/ci/github-slsa-hashes-desktop-dist.sh | 37 +++ .../ci/github-slsa-hashes-release-assets.sh | 30 +++ scripts/ci/slsa-predicate.py | 2 +- scripts/ci/sync-github-release-assets.sh | 7 +- ...ait-github-workflows-and-upload-release.sh | 236 ++++++++++++++++++ 22 files changed, 894 insertions(+), 775 deletions(-) delete mode 100644 .gitea/workflows/bench.yml delete mode 100644 .gitea/workflows/build-test.yml delete mode 100644 .gitea/workflows/build.yml delete mode 100644 .gitea/workflows/ci.yml delete mode 100644 .gitea/workflows/docker.yml create mode 100644 .gitea/workflows/github-release-sync.yml delete mode 100644 .gitea/workflows/rekor-monitor.yml delete mode 100644 .gitea/workflows/scan.yml delete mode 100644 .gitea/workflows/tests.yml create mode 100644 .github/workflows/bench.yml create mode 100644 .github/workflows/build-linux-release.yml create mode 100644 scripts/ci/docker-build-entry.sh create mode 100644 scripts/ci/github-apt-linux-packaging.sh create mode 100644 scripts/ci/github-build-linux-release-assets.sh create mode 100644 scripts/ci/github-draft-release-upload-assets.sh create mode 100644 scripts/ci/github-slsa-hashes-desktop-dist.sh create mode 100644 scripts/ci/github-slsa-hashes-release-assets.sh create mode 100644 scripts/ci/wait-github-workflows-and-upload-release.sh diff --git a/.gitea/workflows/bench.yml b/.gitea/workflows/bench.yml deleted file mode 100644 index 1f628eb..0000000 --- a/.gitea/workflows/bench.yml +++ /dev/null @@ -1,58 +0,0 @@ -name: Benchmarks - -on: - workflow_dispatch: - -jobs: - benchmark: - runs-on: ubuntu-latest - steps: - - name: Checkout - run: | - set -eu - SERVER="${GITEA_SERVER_URL:-${GITHUB_SERVER_URL:-}}" - REPO="${GITEA_REPOSITORY:-${GITHUB_REPOSITORY:-}}" - if [ -z "$SERVER" ] || [ -z "$REPO" ]; then - echo "Checkout: set GITEA_SERVER_URL/GITEA_REPOSITORY or GITHUB_SERVER_URL/GITHUB_REPOSITORY" >&2 - exit 1 - fi - if [ -n "${GITEA_TOKEN:-}" ] || [ -n "${GITHUB_TOKEN:-}" ]; then - TOKEN="${GITEA_TOKEN:-$GITHUB_TOKEN}" - git config --global credential.helper "!f() { echo username=x-access-token; echo password=${TOKEN}; }; f" - fi - git init -q && git remote add origin "${SERVER}/${REPO}.git" - git fetch -q --depth=1 origin "${GITHUB_SHA}" && git checkout -q FETCH_HEAD - - - name: Setup Node.js - run: sh scripts/ci/setup-node.sh 24 - - - name: Setup pnpm - run: sh scripts/ci/setup-pnpm.sh - - - name: Setup Python - run: sh scripts/ci/setup-python.sh 3.14 - - - name: Setup Task - run: sh scripts/ci/setup-task.sh - - - name: Setup Poetry - run: pip install poetry - - - name: Install dependencies - run: | - . scripts/ci/ci-node-path.sh - task install - - - name: Run Benchmarks - id: bench - run: | - . scripts/ci/ci-node-path.sh - set -o pipefail - task bench 2>&1 | tee bench_results.txt - - - name: Run Integrity Tests - id: integrity - run: | - . scripts/ci/ci-node-path.sh - set -o pipefail - task test-integrity 2>&1 | tee -a bench_results.txt diff --git a/.gitea/workflows/build-test.yml b/.gitea/workflows/build-test.yml deleted file mode 100644 index 0b77a9e..0000000 --- a/.gitea/workflows/build-test.yml +++ /dev/null @@ -1,84 +0,0 @@ -name: Build Test - -on: - # Migrated to GitHub Actions (.github/workflows/ci.yml and .github/workflows/build.yml). - # Keep this workflow manual for fallback troubleshooting on Gitea runner. - workflow_dispatch: - -permissions: - contents: read - -jobs: - build-test: - name: Build and Test - runs-on: ubuntu-latest - steps: - - name: Clone Repo - run: | - set -eu - SERVER="${GITEA_SERVER_URL:-${GITHUB_SERVER_URL:-}}" - REPO="${GITEA_REPOSITORY:-${GITHUB_REPOSITORY:-}}" - if [ -z "$SERVER" ] || [ -z "$REPO" ]; then - echo "Checkout: set GITEA_SERVER_URL/GITEA_REPOSITORY or GITHUB_SERVER_URL/GITHUB_REPOSITORY" >&2 - exit 1 - fi - if [ -n "${GITEA_TOKEN:-}" ] || [ -n "${GITHUB_TOKEN:-}" ]; then - TOKEN="${GITEA_TOKEN:-$GITHUB_TOKEN}" - git config --global credential.helper "!f() { echo username=x-access-token; echo password=${TOKEN}; }; f" - fi - git clone "${SERVER}/${REPO}.git" . - git checkout "${GITHUB_SHA}" - - - name: Setup Node.js - run: sh scripts/ci/setup-node.sh 24 - - - name: Setup Python - run: sh scripts/ci/setup-python.sh 3.14 - - - name: Install Poetry - run: python3 -m pip install --upgrade pip poetry>=2.0.0 - - - name: Setup pnpm - run: sh scripts/ci/setup-pnpm.sh - - - name: Install system dependencies - run: | - sh scripts/ci/exec-priv.sh dpkg --add-architecture i386 - sh scripts/ci/exec-priv.sh apt-get update - sh scripts/ci/exec-priv.sh apt-get install -y patchelf libopusfile0 espeak-ng zip rpm elfutils appstream appstream-util - - - name: Setup Task - run: sh scripts/ci/setup-task.sh - - - name: Install dependencies - run: | - . scripts/ci/ci-node-path.sh - task install - - - name: Build Frontend - run: | - . scripts/ci/ci-node-path.sh - task build:fe - - - name: Build Backend (Wheel) - run: task build:wheel - - - name: Ensure cx_Freeze build dependencies - run: poetry run pip install pycparser cffi - - - name: Build Electron App (Linux) - run: | - . scripts/ci/ci-node-path.sh - pnpm run dist:linux-x64 - - - name: Build Electron App (RPM - Experimental) - continue-on-error: true - run: | - . scripts/ci/ci-node-path.sh - task dist:fe:rpm - - - name: Prepare release assets - run: | - mkdir -p release-assets - find dist -maxdepth 1 -type f \( -name "*-linux*.AppImage" -o -name "*-linux*.deb" -o -name "*-linux*.rpm" \) -exec cp {} release-assets/ \; - find python-dist -maxdepth 1 -type f -name "*.whl" -exec cp {} release-assets/ \; diff --git a/.gitea/workflows/build.yml b/.gitea/workflows/build.yml deleted file mode 100644 index 9165225..0000000 --- a/.gitea/workflows/build.yml +++ /dev/null @@ -1,195 +0,0 @@ -# Appimage builds produced by action are broken for now -name: Build and Release - -on: - push: - tags: - - "*" - workflow_dispatch: - inputs: - version: - description: "Release version (e.g., v1.0.0)" - required: false - type: string - build_docker: - description: "Build Docker" - required: false - default: "true" - type: boolean - -permissions: - contents: write - packages: write - -env: - COSIGN_VERSION: "3.0.6" - -jobs: - build: - name: Build and Release - runs-on: ubuntu-latest - steps: - - name: Clone Repo - run: | - set -eu - SERVER="${GITEA_SERVER_URL:-${GITHUB_SERVER_URL:-}}" - REPO="${GITEA_REPOSITORY:-${GITHUB_REPOSITORY:-}}" - if [ -z "$SERVER" ] || [ -z "$REPO" ]; then - echo "Checkout: set GITEA_SERVER_URL/GITEA_REPOSITORY or GITHUB_SERVER_URL/GITHUB_REPOSITORY" >&2 - exit 1 - fi - if [ -n "${GITEA_TOKEN:-}" ] || [ -n "${GITHUB_TOKEN:-}" ]; then - TOKEN="${GITEA_TOKEN:-$GITHUB_TOKEN}" - git config --global credential.helper "!f() { echo username=x-access-token; echo password=${TOKEN}; }; f" - fi - git clone "${SERVER}/${REPO}.git" . - git checkout "${GITHUB_SHA}" - - - name: Determine version - id: version - run: | - VERSION="" - if [ "${{ github.event_name }}" = "workflow_dispatch" ]; then - VERSION="${{ inputs.version || github.event.inputs.version }}" - fi - - if [ -n "$VERSION" ]; then - echo "Using version from input: $VERSION" - elif [[ "${{ github.ref }}" == refs/tags/* ]]; then - VERSION="${GITHUB_REF#refs/tags/}" - if [ -z "${VERSION}" ]; then - VERSION="${{ github.ref_name }}" - fi - echo "Using version from tag: $VERSION" - else - VERSION=$(git rev-parse --short HEAD) - echo "Using version from SHA: $VERSION" - fi - - if [ "${VERSION}" = "master" ] || [ "${VERSION}" = "dev" ] || [ -z "${VERSION}" ]; then - echo "Error: Invalid version '${VERSION}'. Version cannot be a branch name or empty." >&2 - exit 1 - fi - echo "version=${VERSION}" >> $GITHUB_OUTPUT - - - name: Setup Node.js - run: sh scripts/ci/setup-node.sh 24 - - - name: Setup Python - run: sh scripts/ci/setup-python.sh 3.14 - - - name: Install Poetry - run: python3 -m pip install --upgrade pip poetry>=2.0.0 - - - name: Setup pnpm - run: sh scripts/ci/setup-pnpm.sh - - - name: Install system dependencies - run: | - sh scripts/ci/exec-priv.sh dpkg --add-architecture i386 - sh scripts/ci/exec-priv.sh apt-get update - sh scripts/ci/exec-priv.sh apt-get install -y patchelf libopusfile0 espeak-ng zip rpm elfutils - - - name: Setup Task - run: sh scripts/ci/setup-task.sh - - - name: Install dependencies - run: | - . scripts/ci/ci-node-path.sh - task install - - - name: Build Frontend - run: | - . scripts/ci/ci-node-path.sh - task build:fe - - - name: Build Python wheel - run: task build:wheel - - - name: Build Electron App (x64 AppImage + deb) - run: | - . scripts/ci/ci-node-path.sh - pnpm run dist:linux-x64 - - - name: Build Electron App (arm64 AppImage + deb) - run: | - . scripts/ci/ci-node-path.sh - pnpm run dist:linux-arm64 - - - name: Build Electron App (RPM) - continue-on-error: true - run: | - . scripts/ci/ci-node-path.sh - task dist:fe:rpm - - - name: Prepare release assets - run: | - mkdir -p release-assets - find dist -maxdepth 1 -type f \( -name "*-linux*.AppImage" -o -name "*-linux*.deb" -o -name "*-linux*.rpm" \) -exec cp {} release-assets/ \; - find python-dist -maxdepth 1 -type f -name "*.whl" -exec cp {} release-assets/ \; - - # Create frontend zip - (cd meshchatx/public && zip -r ../../release-assets/meshchatx-frontend.zip .) - - # Generate SBOM (CycloneDX) - curl -L -o /tmp/trivy.deb https://git.quad4.io/Quad4-Software/Trivy-Assets/raw/commit/fdfe96b77d2f7b7f5a90cea00af5024c9f728f17/trivy_0.69.3_Linux-64bit.deb - sh scripts/ci/exec-priv.sh dpkg -i /tmp/trivy.deb || sh scripts/ci/exec-priv.sh apt-get install -f -y - trivy fs --format cyclonedx --include-dev-deps --output release-assets/sbom.cyclonedx.json . - - { - echo "## Integrity" - echo "" - echo "Each artifact may have a matching **\`*.cosign.bundle\`** (SLSA v1 provenance via cosign; see \`SECURITY.md\` for verification)." - echo "" - echo "SBOM: **\`sbom.cyclonedx.json\`** (CycloneDX)." - } > release-body.md - - - name: SLSA attestations (cosign) - env: - COSIGN_PRIVATE_KEY: ${{ secrets.COSIGN_PRIVATE_KEY }} - COSIGN_PASSWORD: ${{ secrets.COSIGN_PASSWORD }} - GITHUB_SERVER_URL: ${{ github.server_url }} - GITHUB_REPOSITORY: ${{ github.repository }} - GITHUB_SHA: ${{ github.sha }} - GITHUB_REF: ${{ github.ref }} - GITHUB_RUN_ID: ${{ github.run_id }} - GITHUB_RUN_ATTEMPT: ${{ github.run_attempt }} - GITHUB_WORKFLOW: ${{ github.workflow }} - run: | - set -eu - if [ -z "${COSIGN_PRIVATE_KEY:-}" ]; then - echo "Skipping SLSA attestations: add repository secret COSIGN_PRIVATE_KEY (PEM) to sign releases." - exit 0 - fi - sh scripts/ci/setup-cosign.sh "${COSIGN_VERSION}" - printf '%s\n' "$COSIGN_PRIVATE_KEY" > /tmp/cosign.key - chmod 600 /tmp/cosign.key - export COSIGN_KEY_PATH=/tmp/cosign.key - sh scripts/ci/attest-release-assets.sh ./release-assets - rm -f /tmp/cosign.key - - - name: Validate version - run: | - VERSION="${{ steps.version.outputs.version }}" - if [ -z "${VERSION}" ]; then - echo "Error: Version is empty" >&2 - exit 1 - fi - if [ "${VERSION}" = "master" ] || [ "${VERSION}" = "dev" ]; then - echo "Error: Invalid version '${VERSION}'. Version cannot be a branch name." >&2 - exit 1 - fi - echo "Using version: ${VERSION}" - - - name: Create Release - if: github.event_name == 'push' || github.event_name == 'workflow_dispatch' - uses: https://git.quad4.io/actions/gitea-release-action@4875285c0950474efb7ca2df55233c51333eeb74 # v1 - with: - api_url: ${{ secrets.GITEA_API_URL }} - gitea_token: ${{ secrets.GITEA_TOKEN }} - title: ${{ steps.version.outputs.version }} - tag: ${{ steps.version.outputs.version }} - files: release-assets/* - body_path: "release-body.md" - draft: true - prerelease: false diff --git a/.gitea/workflows/ci.yml b/.gitea/workflows/ci.yml deleted file mode 100644 index a24be5e..0000000 --- a/.gitea/workflows/ci.yml +++ /dev/null @@ -1,176 +0,0 @@ -name: CI - -on: - # Migrated to GitHub Actions (.github/workflows/ci.yml). - # Keep this workflow manual for fallback troubleshooting on Gitea runner. - workflow_dispatch: - -permissions: - contents: read - -jobs: - lint: - runs-on: ubuntu-latest - steps: - - name: Checkout - run: | - set -eu - SERVER="${GITEA_SERVER_URL:-${GITHUB_SERVER_URL:-}}" - REPO="${GITEA_REPOSITORY:-${GITHUB_REPOSITORY:-}}" - if [ -z "$SERVER" ] || [ -z "$REPO" ]; then - echo "Checkout: set GITEA_SERVER_URL/GITEA_REPOSITORY or GITHUB_SERVER_URL/GITHUB_REPOSITORY" >&2 - exit 1 - fi - if [ -n "${GITEA_TOKEN:-}" ] || [ -n "${GITHUB_TOKEN:-}" ]; then - TOKEN="${GITEA_TOKEN:-$GITHUB_TOKEN}" - git config --global credential.helper "!f() { echo username=x-access-token; echo password=${TOKEN}; }; f" - fi - git init -q && git remote add origin "${SERVER}/${REPO}.git" - git fetch -q --depth=1 origin "${GITHUB_SHA}" && git checkout -q FETCH_HEAD - - name: Setup Node.js - run: sh scripts/ci/setup-node.sh 24 - - name: Setup pnpm - run: sh scripts/ci/setup-pnpm.sh - - name: Setup Python - run: sh scripts/ci/setup-python.sh 3.14 - - name: Setup Task - run: sh scripts/ci/setup-task.sh - - name: Setup Poetry - run: pip install poetry - - name: Setup Python environment - run: task setup:be - - name: pip-audit - run: | - poetry run pip install --upgrade "pip>=26.0" pip-audit - poetry run pip-audit - - name: Install Node dependencies - run: | - . scripts/ci/ci-node-path.sh - task deps:fe - - name: Setup Trivy - run: sh scripts/ci/setup-trivy.sh - - name: Trivy filesystem scan (Node deps) - run: sh scripts/ci/trivy-fs-scan.sh - - name: Lint - run: | - . scripts/ci/ci-node-path.sh - set -o pipefail - task lint:all 2>&1 | tee lint_results.txt - - build-frontend: - runs-on: ubuntu-latest - steps: - - name: Checkout - run: | - set -eu - SERVER="${GITEA_SERVER_URL:-${GITHUB_SERVER_URL:-}}" - REPO="${GITEA_REPOSITORY:-${GITHUB_REPOSITORY:-}}" - if [ -z "$SERVER" ] || [ -z "$REPO" ]; then - echo "Checkout: set GITEA_SERVER_URL/GITEA_REPOSITORY or GITHUB_SERVER_URL/GITHUB_REPOSITORY" >&2 - exit 1 - fi - if [ -n "${GITEA_TOKEN:-}" ] || [ -n "${GITHUB_TOKEN:-}" ]; then - TOKEN="${GITEA_TOKEN:-$GITHUB_TOKEN}" - git config --global credential.helper "!f() { echo username=x-access-token; echo password=${TOKEN}; }; f" - fi - git init -q && git remote add origin "${SERVER}/${REPO}.git" - git fetch -q --depth=1 origin "${GITHUB_SHA}" && git checkout -q FETCH_HEAD - - name: Setup Node.js - run: sh scripts/ci/setup-node.sh 24 - - name: Setup pnpm - run: sh scripts/ci/setup-pnpm.sh - - name: Setup Task - run: sh scripts/ci/setup-task.sh - - name: Install dependencies - run: | - . scripts/ci/ci-node-path.sh - task deps:fe - - name: Setup Trivy - run: sh scripts/ci/setup-trivy.sh - - name: Trivy filesystem scan (Node deps) - run: sh scripts/ci/trivy-fs-scan.sh - - name: Determine version - id: version - run: | - SHORT_SHA=$(git rev-parse --short HEAD) - echo "version=${SHORT_SHA}" >> $GITHUB_OUTPUT - - name: Build frontend - run: | - . scripts/ci/ci-node-path.sh - set -o pipefail - task build:fe 2>&1 | tee build_results.txt - env: - VITE_APP_VERSION: ${{ steps.version.outputs.version }} - - test-backend: - runs-on: ubuntu-latest - steps: - - name: Checkout - run: | - set -eu - SERVER="${GITEA_SERVER_URL:-${GITHUB_SERVER_URL:-}}" - REPO="${GITEA_REPOSITORY:-${GITHUB_REPOSITORY:-}}" - if [ -z "$SERVER" ] || [ -z "$REPO" ]; then - echo "Checkout: set GITEA_SERVER_URL/GITEA_REPOSITORY or GITHUB_SERVER_URL/GITHUB_REPOSITORY" >&2 - exit 1 - fi - if [ -n "${GITEA_TOKEN:-}" ] || [ -n "${GITHUB_TOKEN:-}" ]; then - TOKEN="${GITEA_TOKEN:-$GITHUB_TOKEN}" - git config --global credential.helper "!f() { echo username=x-access-token; echo password=${TOKEN}; }; f" - fi - git init -q && git remote add origin "${SERVER}/${REPO}.git" - git fetch -q --depth=1 origin "${GITHUB_SHA}" && git checkout -q FETCH_HEAD - - name: Setup Python - run: sh scripts/ci/setup-python.sh 3.14 - - name: Setup Task - run: sh scripts/ci/setup-task.sh - - name: Compile backend - run: | - set -o pipefail - task compile 2>&1 | tee compile_results.txt - - test-lang: - runs-on: ubuntu-latest - steps: - - name: Checkout - run: | - set -eu - SERVER="${GITEA_SERVER_URL:-${GITHUB_SERVER_URL:-}}" - REPO="${GITEA_REPOSITORY:-${GITHUB_REPOSITORY:-}}" - if [ -z "$SERVER" ] || [ -z "$REPO" ]; then - echo "Checkout: set GITEA_SERVER_URL/GITEA_REPOSITORY or GITHUB_SERVER_URL/GITHUB_REPOSITORY" >&2 - exit 1 - fi - if [ -n "${GITEA_TOKEN:-}" ] || [ -n "${GITHUB_TOKEN:-}" ]; then - TOKEN="${GITEA_TOKEN:-$GITHUB_TOKEN}" - git config --global credential.helper "!f() { echo username=x-access-token; echo password=${TOKEN}; }; f" - fi - git init -q && git remote add origin "${SERVER}/${REPO}.git" - git fetch -q --depth=1 origin "${GITHUB_SHA}" && git checkout -q FETCH_HEAD - - name: Setup Node.js - run: sh scripts/ci/setup-node.sh 24 - - name: Setup pnpm - run: sh scripts/ci/setup-pnpm.sh - - name: Setup Python - run: sh scripts/ci/setup-python.sh 3.14 - - name: Setup Task - run: sh scripts/ci/setup-task.sh - - name: Setup Poetry - run: pip install poetry - - name: Install dependencies - run: | - . scripts/ci/ci-node-path.sh - task install - - name: pip-audit - run: | - poetry run pip install --upgrade "pip>=26.0" pip-audit - poetry run pip-audit - - name: Setup Trivy - run: sh scripts/ci/setup-trivy.sh - - name: Trivy filesystem scan (Node deps) - run: sh scripts/ci/trivy-fs-scan.sh - - name: Run language tests - run: | - . scripts/ci/ci-node-path.sh - set -o pipefail - task test:lang 2>&1 | tee lang_results.txt diff --git a/.gitea/workflows/docker.yml b/.gitea/workflows/docker.yml deleted file mode 100644 index bc27637..0000000 --- a/.gitea/workflows/docker.yml +++ /dev/null @@ -1,102 +0,0 @@ -name: Build and Publish Docker Image - -on: - workflow_dispatch: - push: - tags: - - "*" - -env: - REGISTRY: git.quad4.io - IMAGE_NAME: rns-things/meshchatx - -jobs: - build: - runs-on: ubuntu-latest - permissions: - contents: read - packages: write - - steps: - - name: Checkout repository - run: | - set -eu - SERVER="${GITEA_SERVER_URL:-${GITHUB_SERVER_URL:-}}" - REPO="${GITEA_REPOSITORY:-${GITHUB_REPOSITORY:-}}" - if [ -z "$SERVER" ] || [ -z "$REPO" ]; then - echo "Checkout: set GITEA_SERVER_URL/GITEA_REPOSITORY or GITHUB_SERVER_URL/GITHUB_REPOSITORY" >&2 - exit 1 - fi - if [ -n "${GITEA_TOKEN:-}" ] || [ -n "${GITHUB_TOKEN:-}" ]; then - TOKEN="${GITEA_TOKEN:-$GITHUB_TOKEN}" - git config --global credential.helper "!f() { echo username=x-access-token; echo password=${TOKEN}; }; f" - fi - git init -q && git remote add origin "${SERVER}/${REPO}.git" - git fetch -q --depth=1 origin "${GITHUB_SHA}" && git checkout -q FETCH_HEAD - - - name: Set up Docker (QEMU + Buildx + Login) - run: sh scripts/ci/setup-docker.sh "${{ env.REGISTRY }}" "${{ secrets.REGISTRY_USERNAME }}" "${{ secrets.REGISTRY_PASSWORD }}" - - - name: Download Trivy - run: | - curl -L -o /tmp/trivy.deb https://git.quad4.io/Quad4-Software/Trivy-Assets/raw/commit/fdfe96b77d2f7b7f5a90cea00af5024c9f728f17/trivy_0.69.3_Linux-64bit.deb - sh scripts/ci/exec-priv.sh dpkg -i /tmp/trivy.deb || sh scripts/ci/exec-priv.sh apt-get install -f -y - - - name: Trivy FS scan - run: trivy fs --exit-code 1 . - - - name: Generate Docker tags - id: tags - env: - GITHUB_REF: ${{ github.ref }} - GITHUB_REF_NAME: ${{ github.ref_name }} - GITEA_REF: ${{ github.ref }} - GITEA_REF_NAME: ${{ github.ref_name }} - run: | - sh scripts/ci/docker-tags.sh "${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}" /tmp/docker-tags.txt - TAGS="$(tr '\n' ' ' < /tmp/docker-tags.txt)" - echo "tags=${TAGS}" >> "$GITHUB_OUTPUT" - FIRST_TAG="$(head -1 /tmp/docker-tags.txt | sed 's/^-t //')" - echo "first_tag=${FIRST_TAG}" >> "$GITHUB_OUTPUT" - - - name: OCI labels (build metadata) - id: oci - env: - GITHUB_REF: ${{ github.ref }} - GITHUB_REF_NAME: ${{ github.ref_name }} - GITEA_REF: ${{ github.ref }} - GITEA_REF_NAME: ${{ github.ref_name }} - run: | - set -eu - echo "created=$(date -u +"%Y-%m-%dT%H:%M:%SZ")" >> "$GITHUB_OUTPUT" - echo "revision=$(git rev-parse HEAD)" >> "$GITHUB_OUTPUT" - ref="${GITEA_REF:-${GITHUB_REF:-}}" - ref_name="${GITEA_REF_NAME:-${GITHUB_REF_NAME:-}}" - case "$ref" in - refs/tags/*) - echo "version=${ref_name}" >> "$GITHUB_OUTPUT" - ;; - *) - echo "version=sha-$(git rev-parse --short HEAD)" >> "$GITHUB_OUTPUT" - ;; - esac - - - name: Build and push Docker image - env: - OCI_REVISION: ${{ steps.oci.outputs.revision }} - OCI_VERSION: ${{ steps.oci.outputs.version }} - OCI_CREATED: ${{ steps.oci.outputs.created }} - run: | - set -eu - docker buildx build \ - --platform linux/amd64,linux/arm64 \ - --push \ - --no-cache \ - --build-arg "OCI_REVISION=${OCI_REVISION}" \ - --build-arg "OCI_VERSION=${OCI_VERSION}" \ - --build-arg "OCI_CREATED=${OCI_CREATED}" \ - ${{ steps.tags.outputs.tags }} \ - -f ./Dockerfile . - - - name: Scan Docker image - run: trivy image --exit-code 0 "${{ steps.tags.outputs.first_tag }}" diff --git a/.gitea/workflows/github-release-sync.yml b/.gitea/workflows/github-release-sync.yml new file mode 100644 index 0000000..33f4688 --- /dev/null +++ b/.gitea/workflows/github-release-sync.yml @@ -0,0 +1,70 @@ +# After pushing a tag like release_* to Gitea, wait for GitHub Actions to finish +# (Build release + Build Linux release), then create/update the GitHub release and upload assets. +# +# Repository secrets (Gitea): +# GH_PAT GitHub PAT: contents:write, actions:read (upload release assets, list workflow runs) +# GH_REPOSITORY GitHub repo as owner/name (example: Sudo-Ivan/MeshChatX) +# +# The tag must exist on GitHub (mirror) so workflows run there for the same ref. + +name: Sync tag release to GitHub + +on: + push: + tags: + - "release_*" + workflow_dispatch: + inputs: + tag: + description: Tag to sync to GitHub (required when not run from a tag ref) + required: false + type: string + +permissions: + contents: read + +jobs: + publish-github: + runs-on: ubuntu-latest + timeout-minutes: 240 + steps: + - name: Install tools + run: | + set -eu + sudo apt-get update -y + sudo apt-get install -y --no-install-recommends git curl jq ca-certificates unzip + + - name: Checkout + run: | + set -eu + SERVER="${GITEA_SERVER_URL:-${GITHUB_SERVER_URL:-}}" + REPO="${GITEA_REPOSITORY:-${GITHUB_REPOSITORY:-}}" + if [ -z "$SERVER" ] || [ -z "$REPO" ]; then + echo "Set GITEA_SERVER_URL/GITEA_REPOSITORY (or GitHub equivalents)." >&2 + exit 1 + fi + if [ -n "${GITEA_TOKEN:-}" ] || [ -n "${GITHUB_TOKEN:-}" ]; then + TOKEN="${GITEA_TOKEN:-$GITHUB_TOKEN}" + git config --global credential.helper "!f() { echo username=x-access-token; echo password=${TOKEN}; }; f" + fi + git init -q && git remote add origin "${SERVER}/${REPO}.git" + git fetch -q --depth=1 origin "${GITHUB_SHA}" && git checkout -q FETCH_HEAD + + - name: Publish GitHub release from Actions artifacts + env: + GH_REPOSITORY: ${{ secrets.GH_REPOSITORY }} + GH_PAT: ${{ secrets.GH_PAT }} + TIMEOUT_SEC: "14400" + POLL_INTERVAL: "60" + run: | + set -eu + TAG="${{ github.event.inputs.tag }}" + if [ -z "$TAG" ]; then + TAG="${{ github.ref_name }}" + fi + if [ -z "$TAG" ] || [ "$TAG" = "master" ] || [ "$TAG" = "dev" ]; then + echo "Set workflow input tag= to the GitHub tag, or run this workflow from a tag ref." >&2 + exit 1 + fi + export TAG + bash scripts/ci/wait-github-workflows-and-upload-release.sh diff --git a/.gitea/workflows/rekor-monitor.yml b/.gitea/workflows/rekor-monitor.yml deleted file mode 100644 index 7db80cf..0000000 --- a/.gitea/workflows/rekor-monitor.yml +++ /dev/null @@ -1,37 +0,0 @@ -name: Rekor tree verification - -on: - schedule: - - cron: "23 11 * * 1" - workflow_dispatch: - -permissions: - contents: read - -jobs: - rekor-loginfo: - runs-on: ubuntu-latest - steps: - - name: Clone Repo - run: | - set -eu - SERVER="${GITEA_SERVER_URL:-${GITHUB_SERVER_URL:-}}" - REPO="${GITEA_REPOSITORY:-${GITHUB_REPOSITORY:-}}" - if [ -z "$SERVER" ] || [ -z "$REPO" ]; then - echo "Checkout: set GITEA_SERVER_URL/GITEA_REPOSITORY or GITHUB_SERVER_URL/GITHUB_REPOSITORY" >&2 - exit 1 - fi - if [ -n "${GITEA_TOKEN:-}" ] || [ -n "${GITHUB_TOKEN:-}" ]; then - TOKEN="${GITEA_TOKEN:-$GITHUB_TOKEN}" - git config --global credential.helper "!f() { echo username=x-access-token; echo password=${TOKEN}; }; f" - fi - git init -q && git remote add origin "${SERVER}/${REPO}.git" - git fetch -q --depth=1 origin "${GITHUB_SHA}" && git checkout -q FETCH_HEAD - - - name: Install rekor-cli - run: sh scripts/ci/setup-rekor-cli.sh - - - name: Verify Rekor signed tree head - run: | - set -eu - rekor-cli loginfo --rekor_server "${REKOR_SERVER:-https://rekor.sigstore.dev}" --store_tree_state=false diff --git a/.gitea/workflows/scan.yml b/.gitea/workflows/scan.yml deleted file mode 100644 index 197aa13..0000000 --- a/.gitea/workflows/scan.yml +++ /dev/null @@ -1,51 +0,0 @@ -name: Security Scans - -on: - # Migrated to GitHub Actions; keep manual fallback only on Gitea. - workflow_dispatch: - -permissions: - contents: read - -jobs: - scan: - runs-on: ubuntu-latest - steps: - - name: Checkout - run: | - set -eu - SERVER="${GITEA_SERVER_URL:-${GITHUB_SERVER_URL:-}}" - REPO="${GITEA_REPOSITORY:-${GITHUB_REPOSITORY:-}}" - if [ -z "$SERVER" ] || [ -z "$REPO" ]; then - echo "Checkout: set GITEA_SERVER_URL/GITEA_REPOSITORY or GITHUB_SERVER_URL/GITHUB_REPOSITORY" >&2 - exit 1 - fi - if [ -n "${GITEA_TOKEN:-}" ] || [ -n "${GITHUB_TOKEN:-}" ]; then - TOKEN="${GITEA_TOKEN:-$GITHUB_TOKEN}" - git config --global credential.helper "!f() { echo username=x-access-token; echo password=${TOKEN}; }; f" - fi - git init -q && git remote add origin "${SERVER}/${REPO}.git" - git fetch -q --depth=1 origin "${GITHUB_SHA}" && git checkout -q FETCH_HEAD - - - name: Setup Node.js - run: sh scripts/ci/setup-node.sh 24 - - - name: Setup pnpm - run: sh scripts/ci/setup-pnpm.sh - - - name: Setup Task - run: sh scripts/ci/setup-task.sh - - - name: Install frontend dependencies - run: | - . scripts/ci/ci-node-path.sh - task deps:fe - - - name: Setup Trivy - run: sh scripts/ci/setup-trivy.sh - - - name: Trivy FS scan - run: sh scripts/ci/trivy-fs-scan.sh - - - name: Trivy Dockerfile misconfiguration - run: trivy config --exit-code 1 Dockerfile diff --git a/.gitea/workflows/tests.yml b/.gitea/workflows/tests.yml deleted file mode 100644 index a0a2822..0000000 --- a/.gitea/workflows/tests.yml +++ /dev/null @@ -1,66 +0,0 @@ -name: Tests - -on: - # Migrated to GitHub Actions (.github/workflows/ci.yml). - # Keep this workflow manual for fallback troubleshooting on Gitea runner. - workflow_dispatch: - -permissions: - contents: read - -jobs: - test: - runs-on: ubuntu-latest - steps: - - name: Checkout - run: | - set -eu - SERVER="${GITEA_SERVER_URL:-${GITHUB_SERVER_URL:-}}" - REPO="${GITEA_REPOSITORY:-${GITHUB_REPOSITORY:-}}" - if [ -z "$SERVER" ] || [ -z "$REPO" ]; then - echo "Checkout: set GITEA_SERVER_URL/GITEA_REPOSITORY or GITHUB_SERVER_URL/GITHUB_REPOSITORY" >&2 - exit 1 - fi - if [ -n "${GITEA_TOKEN:-}" ] || [ -n "${GITHUB_TOKEN:-}" ]; then - TOKEN="${GITEA_TOKEN:-$GITHUB_TOKEN}" - git config --global credential.helper "!f() { echo username=x-access-token; echo password=${TOKEN}; }; f" - fi - git init -q && git remote add origin "${SERVER}/${REPO}.git" - git fetch -q --depth=1 origin "${GITHUB_SHA}" && git checkout -q FETCH_HEAD - - - name: Setup Node.js - run: sh scripts/ci/setup-node.sh 24 - - - name: Setup pnpm - run: sh scripts/ci/setup-pnpm.sh - - - name: Setup Python - run: sh scripts/ci/setup-python.sh 3.14 - - - name: Setup Task - run: sh scripts/ci/setup-task.sh - - - name: Setup Poetry - run: pip install poetry - - - name: Install dependencies - run: | - . scripts/ci/ci-node-path.sh - task install - - - name: Install Playwright Chromium - run: | - . scripts/ci/ci-node-path.sh - pnpm exec playwright install chromium --with-deps - - - name: Run tests - run: | - . scripts/ci/ci-node-path.sh - set -o pipefail - task test:all 2>&1 | tee test_results.txt - - - name: Run E2E (Playwright) - run: | - . scripts/ci/ci-node-path.sh - set -o pipefail - CI=1 pnpm run test:e2e diff --git a/.github/workflows/bench.yml b/.github/workflows/bench.yml new file mode 100644 index 0000000..96f1f58 --- /dev/null +++ b/.github/workflows/bench.yml @@ -0,0 +1,63 @@ +# Benchmarks and integrity checks (workflow_dispatch only). +# +# Pinned first-party actions (bump tag and SHA together when upgrading): +# actions/checkout@v6.0.1 8e8c483db84b4bee98b60c0593521ed34d9990e8 +# actions/setup-python@v6.2.0 a309ff8b426b58ec0e2a45f0f869d46889d02405 +# actions/setup-node@v6.1.0 395ad3262231945c25e8478fd5baf05154b1d79f + +name: Benchmarks + +on: + workflow_dispatch: + +permissions: + contents: read + +env: + FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true + PYTHON_VERSION: "3.14" + NODE_VERSION: "24" + POETRY_VERSION: "2.3.4" + PNPM_VERSION: "10.33.0" + +jobs: + bench: + runs-on: ubuntu-latest + timeout-minutes: 60 + steps: + - name: Checkout + uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 + + - name: Set up Python + uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 + with: + python-version: ${{ env.PYTHON_VERSION }} + + - name: Install Poetry (PyPI pin) + env: + POETRY_VERSION: ${{ env.POETRY_VERSION }} + run: bash scripts/ci/github-install-poetry.sh + + - name: Set up Node + uses: actions/setup-node@395ad3262231945c25e8478fd5baf05154b1d79f + with: + node-version: ${{ env.NODE_VERSION }} + + - name: Enable pnpm (corepack) + run: corepack enable && corepack prepare "pnpm@${PNPM_VERSION}" --activate + + - name: Install dependencies + run: bash scripts/ci/github-install-deps.sh + + - name: Setup Task + run: sh scripts/ci/setup-task.sh + + - name: Run benchmarks + run: | + set -euo pipefail + task bench 2>&1 | tee bench_results.txt + + - name: Run integrity tests + run: | + set -euo pipefail + task test-integrity 2>&1 | tee -a bench_results.txt diff --git a/.github/workflows/build-linux-release.yml b/.github/workflows/build-linux-release.yml new file mode 100644 index 0000000..f32ea33 --- /dev/null +++ b/.github/workflows/build-linux-release.yml @@ -0,0 +1,190 @@ +# Linux release binaries (wheel, AppImage, deb, rpm), SBOM, optional cosign bundles, +# SLSA Build Level 3 provenance (slsa-github-generator generic), and a draft GitHub release. +# +# Pinned first-party actions (bump tag and SHA together when upgrading): +# actions/checkout@v6.0.1 8e8c483db84b4bee98b60c0593521ed34d9990e8 +# actions/setup-python@v6.2.0 a309ff8b426b58ec0e2a45f0f869d46889d02405 +# actions/setup-node@v6.1.0 395ad3262231945c25e8478fd5baf05154b1d79f +# actions/upload-artifact@v5.0.0 330a01c490aca151604b8cf639adc76d48f6c5d4 +# actions/download-artifact@v5.0.0 634f93cb2916e3fdff6788551b99b062d0335ce0 +# +# SLSA generator (must stay @vX.Y.Z semver per upstream): +# slsa-framework/slsa-github-generator/generator_generic_slsa3.yml@v2.1.0 + +name: Build Linux release + +on: + push: + tags: + - "*" + workflow_dispatch: + +permissions: + contents: write + actions: write + id-token: write + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +env: + FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true + NODE_OPTIONS: --max-old-space-size=8192 + PYTHON_VERSION: "3.14" + NODE_VERSION: "24" + POETRY_VERSION: "2.3.4" + PNPM_VERSION: "10.33.0" + COSIGN_VERSION: "3.0.6" + +jobs: + frontend: + name: Build frontend artifact + uses: ./.github/workflows/frontend-build.yml + permissions: + contents: read + with: + artifact_name: meshchatx-frontend-linux-rel-${{ github.run_id }}-${{ github.run_attempt }} + retention_days: 7 + pnpm_version: "10.33.0" + + linux-release: + name: Linux release assets + needs: frontend + runs-on: ubuntu-latest + timeout-minutes: 120 + outputs: + hashes: ${{ steps.slsa-hashes.outputs.hashes }} + permissions: + contents: read + actions: write + env: + FRONTEND_ARTIFACT_NAME: ${{ needs.frontend.outputs.artifact_name }} + MESHCHATX_FRONTEND_PREBUILT: "1" + steps: + - name: Checkout + uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 + + - name: Set up Python + uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 + with: + python-version: ${{ env.PYTHON_VERSION }} + + - name: Install Poetry (PyPI pin) + env: + POETRY_VERSION: ${{ env.POETRY_VERSION }} + run: bash scripts/ci/github-install-poetry.sh + + - name: Set up Node + uses: actions/setup-node@395ad3262231945c25e8478fd5baf05154b1d79f + with: + node-version: ${{ env.NODE_VERSION }} + + - name: Enable pnpm (corepack) + run: corepack enable && corepack prepare "pnpm@${PNPM_VERSION}" --activate + + - name: Linux packaging APT dependencies + run: bash scripts/ci/github-apt-linux-packaging.sh + + - name: Install project dependencies + run: bash scripts/ci/github-install-deps.sh + + - name: Download frontend artifact + uses: actions/download-artifact@634f93cb2916e3fdff6788551b99b062d0335ce0 + with: + name: ${{ env.FRONTEND_ARTIFACT_NAME }} + path: meshchatx/public + + - name: Verify frontend artifact contents + run: | + set -euo pipefail + test -f meshchatx/public/index.html + test -d meshchatx/public/assets + test -d meshchatx/public/reticulum-docs-bundled/current + + - name: Setup Task + run: sh scripts/ci/setup-task.sh + + - name: Setup Trivy + run: sh scripts/ci/setup-trivy.sh + + - name: Build release-assets + run: bash scripts/ci/github-build-linux-release-assets.sh + + - name: SLSA subject hashes + id: slsa-hashes + if: startsWith(github.ref, 'refs/tags/') + run: bash scripts/ci/github-slsa-hashes-release-assets.sh + + - name: SLSA attestations (cosign) + env: + COSIGN_PRIVATE_KEY: ${{ secrets.COSIGN_PRIVATE_KEY }} + COSIGN_PASSWORD: ${{ secrets.COSIGN_PASSWORD }} + GITHUB_SERVER_URL: ${{ github.server_url }} + GITHUB_REPOSITORY: ${{ github.repository }} + GITHUB_SHA: ${{ github.sha }} + GITHUB_REF: ${{ github.ref }} + GITHUB_RUN_ID: ${{ github.run_id }} + GITHUB_RUN_ATTEMPT: ${{ github.run_attempt }} + GITHUB_WORKFLOW: ${{ github.workflow }} + COSIGN_VERSION: ${{ env.COSIGN_VERSION }} + run: | + set -eu + if [ -z "${COSIGN_PRIVATE_KEY:-}" ]; then + echo "Skipping cosign attestations (no COSIGN_PRIVATE_KEY)." + exit 0 + fi + sh scripts/ci/setup-cosign.sh "${COSIGN_VERSION}" + printf '%s\n' "$COSIGN_PRIVATE_KEY" > /tmp/cosign.key + chmod 600 /tmp/cosign.key + export COSIGN_KEY_PATH=/tmp/cosign.key + sh scripts/ci/attest-release-assets.sh ./release-assets + rm -f /tmp/cosign.key + + - name: Upload Linux release artifact + uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 + with: + name: meshchatx-linux-release-${{ github.ref_name }}-${{ github.run_id }} + path: release-assets/ + if-no-files-found: error + retention-days: 30 + + slsa-provenance-linux: + name: SLSA provenance (Linux) + needs: [linux-release] + if: startsWith(github.ref, 'refs/tags/') + permissions: + id-token: write + contents: read + actions: read + uses: slsa-framework/slsa-github-generator/.github/workflows/generator_generic_slsa3.yml@v2.1.0 + with: + base64-subjects: ${{ needs.linux-release.outputs.hashes }} + upload-assets: false + provenance-name: meshchatx-linux-${{ github.ref_name }}.intoto.jsonl + + draft-github-release-linux: + name: Draft GitHub release (Linux assets + SLSA) + needs: [linux-release, slsa-provenance-linux] + if: startsWith(github.ref, 'refs/tags/') + runs-on: ubuntu-latest + timeout-minutes: 30 + permissions: + contents: write + steps: + - name: Download Linux release assets + uses: actions/download-artifact@634f93cb2916e3fdff6788551b99b062d0335ce0 + with: + name: meshchatx-linux-release-${{ github.ref_name }}-${{ github.run_id }} + path: upload + + - name: Download SLSA provenance + uses: actions/download-artifact@634f93cb2916e3fdff6788551b99b062d0335ce0 + with: + name: ${{ needs.slsa-provenance-linux.outputs.provenance-name }} + path: upload + + - name: Upload to draft release + env: + GH_TOKEN: ${{ github.token }} + run: bash scripts/ci/github-draft-release-upload-assets.sh upload diff --git a/.github/workflows/build-release.yml b/.github/workflows/build-release.yml index 783668b..0ef27f5 100644 --- a/.github/workflows/build-release.yml +++ b/.github/workflows/build-release.yml @@ -8,6 +8,9 @@ # actions/setup-node@v6.1.0 395ad3262231945c25e8478fd5baf05154b1d79f # actions/upload-artifact@v5.0.0 330a01c490aca151604b8cf639adc76d48f6c5d4 # actions/download-artifact@v5.0.0 634f93cb2916e3fdff6788551b99b062d0335ce0 +# +# SLSA generator (must stay @vX.Y.Z semver per upstream): +# slsa-framework/slsa-github-generator/generator_generic_slsa3.yml@v2.1.0 name: Build release @@ -18,8 +21,9 @@ on: workflow_dispatch: permissions: - contents: read + contents: write actions: write + id-token: write concurrency: group: ${{ github.workflow }}-${{ github.ref }} @@ -145,3 +149,73 @@ jobs: name: ${{ matrix.artifact_prefix }}-${{ github.ref_name }}-${{ github.run_id }} path: dist/ if-no-files-found: warn + + collect-desktop-slsa-subjects: + name: SLSA subjects (Windows + macOS) + needs: [build-release] + if: startsWith(github.ref, 'refs/tags/') + runs-on: ubuntu-latest + outputs: + hashes: ${{ steps.hash.outputs.hashes }} + steps: + - name: Download Windows dist + uses: actions/download-artifact@634f93cb2916e3fdff6788551b99b062d0335ce0 + with: + name: meshchatx-windows-${{ github.ref_name }}-${{ github.run_id }} + path: dl/win + + - name: Download macOS dist + uses: actions/download-artifact@634f93cb2916e3fdff6788551b99b062d0335ce0 + with: + name: meshchatx-macos-${{ github.ref_name }}-${{ github.run_id }} + path: dl/mac + + - name: Hash desktop artifacts + id: hash + run: bash scripts/ci/github-slsa-hashes-desktop-dist.sh dl/win dl/mac + + slsa-provenance-desktop: + name: SLSA provenance (Windows + macOS) + needs: [collect-desktop-slsa-subjects] + if: startsWith(github.ref, 'refs/tags/') + permissions: + id-token: write + contents: read + actions: read + uses: slsa-framework/slsa-github-generator/.github/workflows/generator_generic_slsa3.yml@v2.1.0 + with: + base64-subjects: ${{ needs.collect-desktop-slsa-subjects.outputs.hashes }} + upload-assets: false + provenance-name: meshchatx-desktop-${{ github.ref_name }}.intoto.jsonl + + draft-github-release-desktop: + name: Draft GitHub release (desktop + SLSA) + needs: [build-release, slsa-provenance-desktop] + if: startsWith(github.ref, 'refs/tags/') + runs-on: ubuntu-latest + timeout-minutes: 30 + permissions: + contents: write + steps: + - name: Download Windows dist + uses: actions/download-artifact@634f93cb2916e3fdff6788551b99b062d0335ce0 + with: + name: meshchatx-windows-${{ github.ref_name }}-${{ github.run_id }} + path: upload/win + + - name: Download macOS dist + uses: actions/download-artifact@634f93cb2916e3fdff6788551b99b062d0335ce0 + with: + name: meshchatx-macos-${{ github.ref_name }}-${{ github.run_id }} + path: upload/mac + + - name: Download SLSA provenance + uses: actions/download-artifact@634f93cb2916e3fdff6788551b99b062d0335ce0 + with: + name: ${{ needs.slsa-provenance-desktop.outputs.provenance-name }} + path: upload + + - name: Upload to draft release + env: + GH_TOKEN: ${{ github.token }} + run: bash scripts/ci/github-draft-release-upload-assets.sh upload diff --git a/scripts/ci/docker-build-entry.sh b/scripts/ci/docker-build-entry.sh new file mode 100644 index 0000000..cbc2d75 --- /dev/null +++ b/scripts/ci/docker-build-entry.sh @@ -0,0 +1,71 @@ +#!/usr/bin/env bash +# Run inside Dockerfile.build after COPY. Writes outputs to /artifacts. +# Env: MESHCHATX_BUILD_TARGETS = all | wheel | electron (electron = AppImage+deb per arch + best-effort RPM, no wheel) +set -euo pipefail + +cd /src + +export POETRY_VERSION="${POETRY_VERSION:-2.3.4}" +export PNPM_VERSION="${PNPM_VERSION:-10.33.0}" + +apt-get update -y +apt-get install -y --no-install-recommends \ + ca-certificates curl git jq unzip xz-utils \ + build-essential pkg-config python3-dev + +install_electron_builder_libs() { + apt-get install -y --no-install-recommends \ + libnss3 libatk1.0-0 libatk-bridge2.0-0 libcups2 libdrm2 libgbm1 libasound2 \ + libxcomposite1 libxdamage1 libxfixes3 libxrandr2 libxkbcommon0 \ + libfuse2 zstd libgtk-3-0 +} + +if ! command -v node >/dev/null 2>&1; then + curl -fsSL https://deb.nodesource.com/setup_24.x | bash - + apt-get install -y nodejs +fi + +TASK_VER="${TASK_VERSION:-3.46.4}" +curl -fsSL "https://github.com/go-task/task/releases/download/v${TASK_VER}/task_linux_amd64.tar.gz" \ + | tar xz -C /usr/local/bin task + +corepack enable +corepack prepare "pnpm@${PNPM_VERSION}" --activate + +bash scripts/ci/github-install-poetry.sh + +export TRIVY_SBOM=0 + +targets="${MESHCHATX_BUILD_TARGETS:-all}" +case "$targets" in + wheel) + bash scripts/ci/github-install-deps.sh + export SKIP_ELECTRON=1 + bash scripts/ci/github-build-linux-release-assets.sh + ;; + electron) + install_electron_builder_libs + bash scripts/ci/github-apt-linux-packaging.sh + bash scripts/ci/github-install-deps.sh + task build:fe + export SKIP_WHEEL=1 + bash scripts/ci/github-build-linux-release-assets.sh + ;; + all|*) + install_electron_builder_libs + bash scripts/ci/github-apt-linux-packaging.sh + bash scripts/ci/github-install-deps.sh + task build:fe + export SKIP_WHEEL=0 + export SKIP_ELECTRON=0 + bash scripts/ci/github-build-linux-release-assets.sh + ;; +esac + +mkdir -p /artifacts +sh -c 'cp -a /src/release-assets/. /artifacts/ 2>/dev/null || true' +if [ -z "$(ls -A /artifacts 2>/dev/null || true)" ]; then + echo "docker-build-entry.sh: no files under /artifacts" >&2 + exit 1 +fi +echo "docker-build-entry.sh: artifacts ready under /artifacts" diff --git a/scripts/ci/github-apt-linux-packaging.sh b/scripts/ci/github-apt-linux-packaging.sh new file mode 100644 index 0000000..317e640 --- /dev/null +++ b/scripts/ci/github-apt-linux-packaging.sh @@ -0,0 +1,20 @@ +#!/usr/bin/env bash +# APT packages needed for Linux Electron packaging (AppImage, deb, rpm) on Debian/Ubuntu or in Dockerfile.build (root). +set -euo pipefail + +# shellcheck source=scripts/ci/priv.sh +. "$(dirname "$0")/priv.sh" + +run_priv dpkg --add-architecture i386 || true +run_priv apt-get update -y +run_priv apt-get install -y --no-install-recommends \ + patchelf \ + libopusfile0 \ + espeak-ng \ + zip \ + rpm \ + elfutils \ + fakeroot \ + file \ + libc6:i386 \ + libstdc++6:i386 diff --git a/scripts/ci/github-build-linux-release-assets.sh b/scripts/ci/github-build-linux-release-assets.sh new file mode 100644 index 0000000..beb8cc9 --- /dev/null +++ b/scripts/ci/github-build-linux-release-assets.sh @@ -0,0 +1,60 @@ +#!/usr/bin/env bash +# Build wheel, Linux AppImage/deb (x64 + arm64), optional RPM, frontend zip, and SBOM under ./release-assets/. +# Expects repo root as cwd, dependencies installed (task install / pnpm), and meshchatx/public populated when building Electron. +# Optional: SKIP_WHEEL=1, SKIP_ELECTRON=1, TRIVY_SBOM=0 +set -euo pipefail + +ROOT="$(cd "$(dirname "$0")/../.." && pwd)" +cd "$ROOT" + +# shellcheck source=scripts/ci/ci-node-path.sh +. "$(dirname "$0")/ci-node-path.sh" + +mkdir -p release-assets + +if [ "${SKIP_WHEEL:-0}" != 1 ]; then + echo "Building Python wheel..." + task build:wheel +else + echo "Skipping wheel (SKIP_WHEEL=1)." +fi + +if [ "${SKIP_ELECTRON:-0}" != 1 ]; then + echo "Electron linux x64..." + pnpm run dist:linux-x64 + + echo "Electron linux arm64..." + pnpm run dist:linux-arm64 + + echo "RPM (best-effort)..." + if ! task dist:fe:rpm; then + echo "RPM build failed or skipped; continuing." >&2 + fi +else + echo "Skipping Electron packages (SKIP_ELECTRON=1)." +fi + +echo "Collecting release files..." +find dist -maxdepth 1 -type f \( -name "*-linux*.AppImage" -o -name "*-linux*.deb" -o -name "*-linux*.rpm" \) -exec cp -f {} release-assets/ \; 2>/dev/null || true +find python-dist -maxdepth 1 -type f -name "*.whl" -exec cp -f {} release-assets/ \; 2>/dev/null || true + +if [ -d meshchatx/public ] && [ "${SKIP_ELECTRON:-0}" != 1 ]; then + ( cd meshchatx/public && zip -qr "${ROOT}/release-assets/meshchatx-frontend.zip" . ) +fi + +{ + echo "## Integrity" + echo "" + echo "Each artifact may have a matching **\`*.cosign.bundle\`** when repository signing secrets are configured (see SECURITY.md)." + echo "" + echo "SBOM: **\`sbom.cyclonedx.json\`** (CycloneDX) when produced by CI." +} > release-body.md + +if [ "${TRIVY_SBOM:-1}" != 0 ] && command -v trivy >/dev/null 2>&1; then + echo "Generating SBOM..." + trivy fs --format cyclonedx --include-dev-deps --output release-assets/sbom.cyclonedx.json . +else + echo "Skipping SBOM (trivy not on PATH or TRIVY_SBOM=0)." >&2 +fi + +echo "github-build-linux-release-assets.sh: done; see ./release-assets/" diff --git a/scripts/ci/github-draft-release-upload-assets.sh b/scripts/ci/github-draft-release-upload-assets.sh new file mode 100644 index 0000000..3a79c7e --- /dev/null +++ b/scripts/ci/github-draft-release-upload-assets.sh @@ -0,0 +1,31 @@ +#!/usr/bin/env bash +# Create the GitHub release as a draft if missing, then upload every file in DIR to that tag. +# Requires: gh, GH_TOKEN. TAG from TAG or GITHUB_REF_NAME. +set -euo pipefail + +DIR="${1:?path to directory of files to upload}" +TAG="${TAG:-${GITHUB_REF_NAME:?set TAG or GITHUB_REF_NAME}}" + +if ! command -v gh >/dev/null 2>&1; then + echo "gh is required" >&2 + exit 1 +fi + +if [ -z "${GH_TOKEN:-}" ]; then + echo "GH_TOKEN is required" >&2 + exit 1 +fi + +export GH_TOKEN + +if ! gh release view "$TAG" >/dev/null 2>&1; then + gh release create "$TAG" --draft --title "$TAG" --notes "Automated draft release. Review assets and provenance before publishing." +fi + +mapfile -t files < <(find "$DIR" -type f) +if [ "${#files[@]}" -eq 0 ]; then + echo "No files under ${DIR}" >&2 + exit 1 +fi + +gh release upload "$TAG" "${files[@]}" --clobber diff --git a/scripts/ci/github-install-deps.sh b/scripts/ci/github-install-deps.sh index 23da8bb..aaeff74 100755 --- a/scripts/ci/github-install-deps.sh +++ b/scripts/ci/github-install-deps.sh @@ -5,6 +5,9 @@ set -euo pipefail ROOT="$(cd "$(dirname "$0")/../.." && pwd)" cd "$ROOT" +# shellcheck source=scripts/ci/priv.sh +. "$(dirname "$0")/priv.sh" + export GIT_TERMINAL_PROMPT=0 # pycodec2 builds against libcodec2. Export for this step and persist to GITHUB_ENV so @@ -30,8 +33,8 @@ fi # Linux runners do not ship these by default, so backend Opus encode tests fail # with PyOggError until the shared libraries are present. if [[ "$(uname -s)" == "Linux" ]] && command -v apt-get >/dev/null 2>&1; then - sudo apt-get update -y - sudo apt-get install -y libopus0 libogg0 + run_priv apt-get update -y + run_priv apt-get install -y libopus0 libogg0 fi python -m poetry check --lock diff --git a/scripts/ci/github-slsa-hashes-desktop-dist.sh b/scripts/ci/github-slsa-hashes-desktop-dist.sh new file mode 100644 index 0000000..192da8c --- /dev/null +++ b/scripts/ci/github-slsa-hashes-desktop-dist.sh @@ -0,0 +1,37 @@ +#!/usr/bin/env bash +# Emit base64-encoded sha256sum lines for Electron outputs under downloaded artifact roots. +# Subject paths are the relative paths passed to sha256sum (stable for slsa-verifier). +set -euo pipefail + +if [ "$#" -lt 1 ]; then + echo "usage: $0 [root...]" >&2 + exit 1 +fi + +roots=() +for d in "$@"; do + [ -d "$d" ] && roots+=("$d") +done +if [ "${#roots[@]}" -eq 0 ]; then + echo "No existing directories in: $*" >&2 + exit 1 +fi + +tmp="$(mktemp)" +trap 'rm -f "$tmp"' EXIT + +find "${roots[@]}" -type f \( \ + -name '*.exe' -o -name '*.dmg' -o -name '*.blockmap' -o -name '*.yml' -o -name '*.yaml' \ + \) ! -path '*/.*' -print | LC_ALL=C sort | xargs -r sha256sum >"$tmp" + +if [ ! -s "$tmp" ]; then + echo "No matching dist files under: ${roots[*]}" >&2 + exit 1 +fi + +b64="$(base64 -w0 <"$tmp")" +if [ -n "${GITHUB_OUTPUT:-}" ]; then + echo "hashes=${b64}" >>"$GITHUB_OUTPUT" +else + printf '%s\n' "$b64" +fi diff --git a/scripts/ci/github-slsa-hashes-release-assets.sh b/scripts/ci/github-slsa-hashes-release-assets.sh new file mode 100644 index 0000000..2cb36c0 --- /dev/null +++ b/scripts/ci/github-slsa-hashes-release-assets.sh @@ -0,0 +1,30 @@ +#!/usr/bin/env bash +# Emit base64-encoded sha256sum lines for files in ./release-assets (SLSA generic generator input). +# Excludes *.cosign.bundle. Writes "hashes=" to GITHUB_OUTPUT when set. +set -euo pipefail + +cd "$(dirname "$0")/../.." +if [ ! -d release-assets ]; then + echo "release-assets/ missing" >&2 + exit 1 +fi + +tmp="$(mktemp)" +trap 'rm -f "$tmp"' EXIT + +( + cd release-assets + find . -maxdepth 1 -type f ! -name '*.cosign.bundle' -printf '%P\0' | sort -z | xargs -0 sha256sum +) >"$tmp" + +if [ ! -s "$tmp" ]; then + echo "No files to hash under release-assets/" >&2 + exit 1 +fi + +b64="$(base64 -w0 <"$tmp")" +if [ -n "${GITHUB_OUTPUT:-}" ]; then + echo "hashes=${b64}" >>"$GITHUB_OUTPUT" +else + printf '%s\n' "$b64" +fi diff --git a/scripts/ci/slsa-predicate.py b/scripts/ci/slsa-predicate.py index f9205e5..dd0ac4c 100644 --- a/scripts/ci/slsa-predicate.py +++ b/scripts/ci/slsa-predicate.py @@ -35,7 +35,7 @@ def _build_type() -> str: os.environ.get("GITHUB_REPOSITORY") or os.environ.get("GITEA_REPOSITORY") or "" ) if server and repo: - return f"{server}/{repo}/.gitea/workflows/build.yml" + return f"{server}/{repo}/.github/workflows/build-linux-release.yml" return "https://slsa.dev/provenance/v1" diff --git a/scripts/ci/sync-github-release-assets.sh b/scripts/ci/sync-github-release-assets.sh index e2a7c84..5c3a5b4 100644 --- a/scripts/ci/sync-github-release-assets.sh +++ b/scripts/ci/sync-github-release-assets.sh @@ -1,7 +1,10 @@ #!/usr/bin/env bash -# Download Windows/macOS build artifacts from GitHub Actions (build-release workflow) +# Download Windows/macOS build artifacts from GitHub Actions (build-release.yml) # and attach them to an existing Gitea release. Best-effort: missing platforms are skipped. # +# Primary CI and Linux release binaries live under .github/workflows/ (see README.md). +# Linux artifacts are produced by build-linux-release.yml on GitHub if you extend this script. +# # Required env: TAG, GITHUB_REPOSITORY, GITHUB_PAT, GITEA_API_URL, GITEA_REPOSITORY, GITEA_TOKEN set -euo pipefail @@ -112,7 +115,7 @@ fi REL_JSON=$(curl -sS "${AUTH_GITEA[@]}" "${GITEA_API_URL}/api/v1/repos/${GITEA_REPOSITORY}/releases/tags/${TAG}") REL_ID=$(printf '%s' "$REL_JSON" | jq -r '.id // empty') if [ -z "$REL_ID" ] || [ "$REL_ID" = "null" ]; then - log "Error: No Gitea release for tag '${TAG}'. Create the release first (e.g. push the tag so .gitea/workflows/build.yml runs)." + log "Error: No Gitea release for tag '${TAG}'. Create the Gitea release first, then re-run this script." exit 1 fi diff --git a/scripts/ci/wait-github-workflows-and-upload-release.sh b/scripts/ci/wait-github-workflows-and-upload-release.sh new file mode 100644 index 0000000..d10d1fd --- /dev/null +++ b/scripts/ci/wait-github-workflows-and-upload-release.sh @@ -0,0 +1,236 @@ +#!/usr/bin/env bash +# Wait for successful GitHub Actions runs for TAG, download artifacts from listed workflows, +# then create or update a GitHub release and upload binaries. +# +# Required env: TAG, GH_REPOSITORY (owner/repo), GH_PAT +# Optional: WORKFLOWS (space-separated, default: build-release.yml build-linux-release.yml) +# TIMEOUT_SEC (default 14400), POLL_INTERVAL (default 60), DRAFT (default false) +# RELEASE_BODY_FILE (path to markdown; default tries ./release-body.md from cwd) +set -euo pipefail + +TAG="${TAG:?set TAG (e.g. v1.2.3 or release_1.2.3)}" +GH_REPOSITORY="${GH_REPOSITORY:?set GH_REPOSITORY to owner/repo on github.com}" +GH_PAT="${GH_PAT:?set GH_PAT (fine-grained or classic PAT with contents:write, actions:read)}" +WORKFLOWS="${WORKFLOWS:-build-release.yml build-linux-release.yml}" +TIMEOUT_SEC="${TIMEOUT_SEC:-14400}" +POLL_INTERVAL="${POLL_INTERVAL:-60}" +DRAFT="${DRAFT:-false}" + +GH_API="https://api.github.com/repos/${GH_REPOSITORY}" +AUTH=(-H "Authorization: Bearer ${GH_PAT}" -H "Accept: application/vnd.github+json" -H "X-GitHub-Api-Version: 2022-11-28") + +WORKDIR=$(mktemp -d) +trap 'rm -rf "${WORKDIR}"' EXIT + +log() { + printf '%s\n' "$*" >&2 +} + +if ! command -v jq >/dev/null 2>&1; then + log "Error: jq is required." + exit 1 +fi + +enc_tag() { + printf '%s' "$1" | jq -sRr @uri +} + +latest_success_run_id() { + local wf="$1" + local tag="$2" + local tag_enc run_id runs_json commit_sha runs_json2 + + tag_enc=$(enc_tag "$tag") + run_id="" + if runs_json=$(curl -sS -f "${AUTH[@]}" "${GH_API}/actions/workflows/${wf}/runs?event=push&branch=${tag_enc}&per_page=30" 2>/dev/null); then + run_id=$(printf '%s' "$runs_json" | jq -r ' + [.workflow_runs[] | select(.conclusion == "success")] + | sort_by(.created_at) | reverse | .[0].id // empty + ') + fi + + if [ -z "$run_id" ]; then + if ! commit_sha=$(curl -sS -f "${AUTH[@]}" "${GH_API}/commits/${tag_enc}" | jq -r '.sha // empty'); then + commit_sha="" + fi + if [ -n "$commit_sha" ] && runs_json2=$(curl -sS -f "${AUTH[@]}" "${GH_API}/actions/workflows/${wf}/runs?per_page=80" 2>/dev/null); then + run_id=$(printf '%s' "$runs_json2" | jq -r --arg sha "$commit_sha" ' + [.workflow_runs[] | select(.head_sha == $sha and .conclusion == "success")] + | sort_by(.created_at) | reverse | .[0].id // empty + ') + fi + fi + printf '%s' "$run_id" +} + +deadline=$(( $(date +%s) + TIMEOUT_SEC )) +declare -A RUN_IDS=() + +log "Waiting for workflows (${WORKFLOWS}) on tag ${TAG} (timeout ${TIMEOUT_SEC}s)..." + +while [ "$(date +%s)" -lt "$deadline" ]; do + all_set=1 + for wf in $WORKFLOWS; do + rid=$(latest_success_run_id "$wf" "$TAG") + if [ -n "$rid" ]; then + RUN_IDS["$wf"]=$rid + else + all_set=0 + fi + done + if [ "$all_set" = 1 ]; then + break + fi + log "Not all workflows succeeded yet; sleeping ${POLL_INTERVAL}s..." + sleep "$POLL_INTERVAL" +done + +for wf in $WORKFLOWS; do + if [ -z "${RUN_IDS[$wf]:-}" ]; then + log "Timeout or missing successful run for ${wf} (tag=${TAG})." + exit 1 + fi + log "Using ${wf} run_id=${RUN_IDS[$wf]}" +done + +STAGE="${WORKDIR}/stage" +mkdir -p "$STAGE" + +download_and_stage_run() { + local wf="$1" + local rid="$2" + local art_json n + + art_json=$(curl -sS "${AUTH[@]}" "${GH_API}/actions/runs/${rid}/artifacts?per_page=100" || true) + n=$(printf '%s' "${art_json:-{}}" | jq -r '(.artifacts // []) | length') + if [ "${n:-0}" -eq 0 ]; then + log "No artifacts for ${wf} run ${rid}." + return + fi + + printf '%s' "$art_json" | jq -r '.artifacts[] | "\(.name)|\(.archive_download_url)"' | while IFS='|' read -r art_name dl_url; do + case "$wf" in + build-release.yml) + case "$art_name" in + meshchatx-windows-*|meshchatx-macos-*) ;; + *) + log "Skipping artifact ${art_name} for ${wf}" + continue + ;; + esac + ;; + build-linux-release.yml) + case "$art_name" in + meshchatx-linux-release-*) ;; + *) + log "Skipping artifact ${art_name} for ${wf}" + continue + ;; + esac + ;; + *) + log "Unknown workflow file in download filter: ${wf}" >&2 + exit 1 + ;; + esac + + zip_path="${WORKDIR}/$(echo "$art_name" | tr '/' '_').zip" + log "Downloading ${art_name}..." + if ! curl -sS -fL "${AUTH[@]}" -o "$zip_path" "$dl_url"; then + log "Warning: download failed for ${art_name}" + continue + fi + ex="${WORKDIR}/ex-${art_name}" + mkdir -p "$ex" + if ! unzip -q -o "$zip_path" -d "$ex" 2>/dev/null; then + log "Warning: unzip failed for ${art_name}" + continue + fi + find "$ex" -type f \( \ + -name '*.AppImage' -o -name '*.deb' -o -name '*.rpm' -o -name '*.whl' -o \ + -name '*.exe' -o -name '*.dmg' -o -name '*.blockmap' -o \ + -name 'latest*.yml' -o -name 'latest*.yaml' -o -name '*-linux.yml' -o -name '*-linux.yaml' -o \ + -name 'meshchatx-frontend.zip' -o -name 'sbom.cyclonedx.json' -o -name '*.cosign.bundle' -o -name '*.intoto.jsonl' \ + \) -print0 2>/dev/null | while IFS= read -r -d '' f; do + cp -f "$f" "${STAGE}/$(basename "$f")" + done + done +} + +for wf in $WORKFLOWS; do + download_and_stage_run "$wf" "${RUN_IDS[$wf]}" +done + +file_count=$(find "$STAGE" -mindepth 1 -maxdepth 1 -type f 2>/dev/null | wc -l) +file_count=${file_count//[[:space:]]/} +if [ "${file_count:-0}" -eq 0 ]; then + log "No release files staged after downloads." + exit 1 +fi + +BODY_FILE="${RELEASE_BODY_FILE:-}" +if [ -z "$BODY_FILE" ] && [ -f "./release-body.md" ]; then + BODY_FILE="./release-body.md" +fi +BODY_JSON=$(jq -n '""') +if [ -n "$BODY_FILE" ] && [ -f "$BODY_FILE" ]; then + BODY_JSON=$(jq -Rs . < "$BODY_FILE") +fi + +log "Creating or locating GitHub release for tag ${TAG}..." +REL_GET=$(curl -sS -w '%{http_code}' -o "${WORKDIR}/rel.json" "${AUTH[@]}" "${GH_API}/releases/tags/${TAG}" || true) +HTTP="${REL_GET: -3}" +REL_ID="" +if [ "$HTTP" = "200" ]; then + REL_ID=$(jq -r '.id // empty' "${WORKDIR}/rel.json") + UPLOAD_URL=$(jq -r '.upload_url // empty' "${WORKDIR}/rel.json") + log "Release exists id=${REL_ID}" +else + if [ "$DRAFT" = "true" ] || [ "$DRAFT" = "1" ]; then + draft_json="true" + else + draft_json="false" + fi + payload=$(jq -n \ + --arg tag "$TAG" \ + --arg name "$TAG" \ + --argjson draft "$draft_json" \ + --argjson body "$BODY_JSON" \ + '{tag_name: $tag, name: $name, body: body, draft: draft}') + if ! curl -sS -f "${AUTH[@]}" -X POST -H "Content-Type: application/json" \ + -d "$payload" "${GH_API}/releases" -o "${WORKDIR}/newrel.json"; then + log "Failed to create release for ${TAG}" + exit 1 + fi + REL_ID=$(jq -r '.id // empty' "${WORKDIR}/newrel.json") + UPLOAD_URL=$(jq -r '.upload_url // empty' "${WORKDIR}/newrel.json") + log "Created release id=${REL_ID}" +fi + +if [ -z "$REL_ID" ] || [ -z "$UPLOAD_URL" ]; then + log "Could not resolve release id or upload_url." + exit 1 +fi + +BASE_UPLOAD="${UPLOAD_URL%\{*}" + +EXISTING_NAMES=$(curl -sS "${AUTH[@]}" "${GH_API}/releases/${REL_ID}/assets" | jq -c '[.[].name]' 2>/dev/null || echo '[]') + +while IFS= read -r -d '' f; do + base=$(basename "$f") + if jq -n -e --argjson names "$EXISTING_NAMES" --arg n "$base" '$names | index($n) != null' >/dev/null 2>&1; then + log "Skipping existing asset: ${base}" + continue + fi + enc=$(printf '%s' "$base" | jq -sRr @uri) + log "Uploading ${base}..." + if ! curl -sS -f "${AUTH[@]}" -X POST \ + -H "Content-Type: application/octet-stream" \ + --data-binary @"$f" \ + "${BASE_UPLOAD}?name=${enc}" >/dev/null; then + log "Upload failed for ${base}" + exit 1 + fi +done < <(find "$STAGE" -maxdepth 1 -type f -print0) + +log "Done publishing assets to GitHub release ${TAG}."