diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..06e188d --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,150 @@ +name: Test, build and publish + +on: + push: + pull_request: + workflow_dispatch: + +permissions: + contents: read + +concurrency: + group: ci-${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: ${{ github.event_name == 'pull_request' }} + +env: + CARGO_INCREMENTAL: '0' + CARGO_PROFILE_DEV_DEBUG: '0' + CARGO_PROFILE_TEST_DEBUG: '0' + RUST_BACKTRACE: '1' + +jobs: + verify: + name: All tests and Linux binary + runs-on: ubuntu-24.04 + timeout-minutes: 90 + steps: + - uses: actions/checkout@v4 + with: + persist-credentials: false + - name: Install native test dependencies and make room for 10 GiB tests + run: | + sudo rm -rf /usr/share/dotnet /usr/local/lib/android /opt/ghc + sudo apt-get update + sudo apt-get install -y --no-install-recommends build-essential pkg-config libssl-dev libicu-dev lua5.4 liblua5.4-dev lua-filesystem lua-expat lua-socket lua-sec openssl + rust_version=$(python3 -c 'import tomllib; print(tomllib.load(open("rust-toolchain.toml", "rb"))["toolchain"]["channel"])') + rustup toolchain install "$rust_version" --profile minimal --component rustfmt --component clippy + - uses: actions/setup-python@v5 + with: + python-version: '3.12' + - uses: actions/setup-go@v5 + with: + go-version: '1.24.3' + cache: false + - name: Build exact native Prosody reference + run: bash scripts/ci/prepare-reference.sh "$RUNNER_TEMP/reference" + - name: Build genuine MinIO + run: | + bash scripts/ci/prepare-minio.sh "$RUNNER_TEMP/minio" + go clean -cache -modcache + df -h . "$RUNNER_TEMP" + python3 -c 'import shutil; assert shutil.disk_usage(".").free >= 20 * 2**30, "Need 20 GiB free for actual upload tests"' + - name: Run every test, native oracle, load and real 10 GiB uploads + env: + PROSODY_REFERENCE_SOURCE: ${{ runner.temp }}/reference/prosody-cafd74b9c5ea + PROSODY_REFERENCE_LUA: lua5.4 + MINIO_BINARY: ${{ runner.temp }}/minio + run: | + mkdir -p ci-results + set -o pipefail + bash scripts/ci/verify-all.sh 2>&1 | tee ci-results/verify.log + - name: Package tested binary + run: | + mkdir -p dist + cp target/release/rust-xmpp dist/rust-xmpp + chmod 0755 dist/rust-xmpp + dist/rust-xmpp --version + (cd dist && sha256sum rust-xmpp > SHA256SUMS) + - name: Save binary + uses: actions/upload-artifact@v4 + with: + name: rust-xmpp-linux-amd64 + path: dist/ + if-no-files-found: error + retention-days: 30 + - name: Save test reports even on failure + if: always() + uses: actions/upload-artifact@v4 + with: + name: test-results + path: | + ci-results/ + /tmp/muc-sm-load.json + if-no-files-found: warn + retention-days: 14 + + image: + name: Container smoke test and Gitea publish + needs: verify + runs-on: ubuntu-24.04 + timeout-minutes: 20 + # Only the publication job needs registry credentials. PRs still build and test images. + env: + GITEA_REGISTRY: ${{ vars.GITEA_REGISTRY }} + GITEA_IMAGE: ${{ vars.GITEA_IMAGE }} + DEFAULT_BRANCH: ${{ github.event.repository.default_branch }} + PUBLISH: ${{ github.event_name != 'pull_request' && (github.ref == format('refs/heads/{0}', github.event.repository.default_branch) || startsWith(github.ref, 'refs/tags/v')) }} + steps: + - uses: actions/checkout@v4 + with: + persist-credentials: false + - uses: actions/download-artifact@v4 + with: + name: rust-xmpp-linux-amd64 + path: dist + - name: Verify downloaded executable + run: | + (cd dist && sha256sum --check SHA256SUMS) + chmod 0755 dist/rust-xmpp + - uses: docker/setup-buildx-action@v3 + - name: Build exact tested binary into runtime image + uses: docker/build-push-action@v6 + with: + context: . + target: ci-runtime + platforms: linux/amd64 + load: true + push: false + tags: rust-xmpp:ci + - name: Smoke-test runtime image + run: | + docker run --rm rust-xmpp:ci --version + docker run --rm --entrypoint /usr/local/bin/prosodyctl rust-xmpp:ci --version + id=$(docker create rust-xmpp:ci) + trap 'docker rm "$id" >/dev/null' EXIT + docker cp "$id:/usr/local/bin/rust-xmpp" /tmp/container-rust-xmpp + cmp dist/rust-xmpp /tmp/container-rust-xmpp + - name: Validate publication configuration + if: env.PUBLISH == 'true' + env: + REGISTRY_USER: ${{ secrets.GITEA_REGISTRY_USERNAME }} + REGISTRY_TOKEN: ${{ secrets.GITEA_REGISTRY_TOKEN }} + run: python3 scripts/ci/image-tags.py + id: image + - name: Login to private Gitea registry + if: env.PUBLISH == 'true' + uses: docker/login-action@v3 + with: + registry: ${{ vars.GITEA_REGISTRY }} + username: ${{ secrets.GITEA_REGISTRY_USERNAME }} + password: ${{ secrets.GITEA_REGISTRY_TOKEN }} + - name: Push the smoke-tested image + if: env.PUBLISH == 'true' + env: + IMAGE_TAGS: ${{ steps.image.outputs.tags }} + run: | + while IFS= read -r tag; do + docker tag rust-xmpp:ci "$tag" + docker push "$tag" + printf 'Published `%s`\n' "$tag" >> "$GITHUB_STEP_SUMMARY" + done <<< "$IMAGE_TAGS" diff --git a/Dockerfile b/Dockerfile index 4b45559..e8782af 100644 --- a/Dockerfile +++ b/Dockerfile @@ -4,14 +4,23 @@ COPY Cargo.toml Cargo.lock ./ COPY src ./src RUN cargo build --release --locked -FROM debian:trixie-slim +FROM debian:trixie-slim AS runtime-base RUN apt-get update \ && apt-get install -y --no-install-recommends ca-certificates gosu \ && rm -rf /var/lib/apt/lists/* -COPY --from=builder /src/target/release/rust-xmpp /usr/local/bin/rust-xmpp -RUN ln -s /usr/local/bin/rust-xmpp /usr/local/bin/prosodyctl COPY scripts/docker-entrypoint.sh /usr/local/bin/rust-xmpp-entrypoint RUN chmod 0755 /usr/local/bin/rust-xmpp-entrypoint WORKDIR /work ENTRYPOINT ["rust-xmpp-entrypoint"] CMD ["serve", "/etc/prosody/prosody.cfg.lua", "--experimental"] + +# CI downloads the exact executable produced by the verification job. +FROM runtime-base AS ci-runtime +COPY dist/rust-xmpp /usr/local/bin/rust-xmpp +RUN chmod 0755 /usr/local/bin/rust-xmpp \ + && ln -s /usr/local/bin/rust-xmpp /usr/local/bin/prosodyctl + +# Default local build still compiles directly from source. +FROM runtime-base AS runtime +COPY --from=builder /src/target/release/rust-xmpp /usr/local/bin/rust-xmpp +RUN ln -s /usr/local/bin/rust-xmpp /usr/local/bin/prosodyctl diff --git a/README.md b/README.md index 4d6b64a..d88a3be 100644 --- a/README.md +++ b/README.md @@ -232,3 +232,9 @@ recovery Rust служит собственный журнал. Перед от не выполняются; неизвестные явно включённые MUC-модули отклоняются при запуске. Результаты проверки — в [docs/VALIDATION-0.8.0.md](docs/VALIDATION-0.8.0.md). + +## GitHub Actions и Gitea Registry + +Полные тесты, Linux binary artifact и Docker push после проверок: +[настройка workflow](docs/GITHUB-ACTIONS.md). Registry и credentials задаются +через GitHub Actions variables/secrets. diff --git a/docs/CI-VALIDATION.md b/docs/CI-VALIDATION.md new file mode 100644 index 0000000..c99dd1f --- /dev/null +++ b/docs/CI-VALIDATION.md @@ -0,0 +1,17 @@ +# Проверка изменения CI + +2026-10-20. Изменяется только CI, упаковка Docker и документация; Rust source +и версия0.8.0 не менялись. Предыдущие293 теста относятся к этому Rust source. + +Локально успешно: + +- actionlint1.7.7: `.github/workflows/ci.yml` без ошибок; +- bash -n для всех трёх CI shell scripts; +- Python compile и проверка генерации default-branch/version/SHA tags; +- отклонение чужой ветки, несовпадающей версии, URL вместо hostname, + некорректного image path и отсутствующего token; +- SHA-256 bundled reference archive совпадает с ожидаемым. + +Docker daemon отсутствует; GitHub Actions и реальный push в Gitea здесь +не запускались. Результат следует проверить первым workflow run после +добавления репозитория, variables и secrets по GITHUB-ACTIONS.md. diff --git a/docs/GITHUB-ACTIONS.md b/docs/GITHUB-ACTIONS.md new file mode 100644 index 0000000..de0b63e --- /dev/null +++ b/docs/GITHUB-ACTIONS.md @@ -0,0 +1,66 @@ +# GitHub Actions → private Gitea Registry + +Workflow: `.github/workflows/ci.yml`. Push, pull request и ручной запуск выполняют +проверки; публикация разрешена только для основной ветки репозитория и тегов +`v<версия Cargo.toml>`. PR, включая PR из forks, не получает registry credentials. + +## Настройка GitHub + +Settings → Secrets and variables → Actions: + +| Тип | Имя | Значение | +|---|---|---| +| Variable | `GITEA_REGISTRY` | `gitea.example.org` или `gitea.example.org:5000`, без `https://` и пути | +| Variable | `GITEA_IMAGE` | `owner/rust-xmpp`, lowercase, без registry и тега | +| Secret | `GITEA_REGISTRY_USERNAME` | Имя пользователя Gitea с правом публикации пакетов owner | +| Secret | `GITEA_REGISTRY_TOKEN` | Personal access token с правом записи packages | + +Создайте private owner/organization и проверьте права чтения пакета в Gitea: +аутентифицированный push сам по себе не меняет публичность owner/package. +Registry должен быть доступен GitHub-hosted runner по HTTPS с доверенным CA. +Для registry только в LAN потребуется runner с доступом к этой сети; настройки +runs-on для image job можно заменить на labels доверенного self-hosted runner. +Не запускайте PR-код на привилегированном self-hosted runner без изоляции. + +## Проверки и результат + +Job `verify` на Ubuntu24.04 ставит Rust из `rust-toolchain.toml`, Python3.12, +Lua5.4, OpenSSL/ICU и собирает оригинальную bundled Prosody cafd74b9c5ea. +MinIO собирается из unmodified upstream RELEASE.2025-04-22T22-12-26Z, +Go1.24.3, с проверкой SHA-256 исходного архива. Sandbox adapter не применяется. + +`scripts/ci/verify-all.sh` запускает: + +- fmt, все обычные Rust tests, Clippy и release build; +- все сетевые проверки из verify-local.sh, native module/storage и federation; +- обе нагрузки500TLS, в том числе MUC+SM SIGKILL/resume; +- оставшиеся ignored tests: AF_UNIX admin socket и archive>128MiB; +- реальные10GiB PUT/GET через MinIO и через local HTTPS backend, последовательно. + +Таким образом отсутствие native reference или MinIO является ошибкой, а не skip. +Нужно не менее20GiB свободного диска перед проверками. На одноразовом hosted runner +удаляются неиспользуемые Android/.NET/GHC SDK; Go caches очищаются после сборки MinIO. +Тесты могут занимать десятки минут. Timeout verify job —90минут. + +Artifact `rust-xmpp-linux-amd64`: проверенный release executable и SHA256SUMS. +Artifact `test-results`: полный лог и JSON-отчёты, также при ошибке теста. +Job `image` скачивает именно этот executable, проверяет checksum и собирает +Docker target `ci-runtime`. В образе выполняются `rust-xmpp --version` и +`prosodyctl --version`, затем извлечённый executable сравнивается побайтно с artifact. +Image smoke проходит и на PR. Только после успеха выполняются login/push. + +Теги: + +- `sha-<полный40символьныйcommit>` — каждый публикуемый commit; +- `latest` — только основная ветка; +- `v0.8.0` — пример version tag; он должен совпадать с Cargo.toml. + +Version tag не меняет latest; prerelease также не перезаписывает latest. +Собирается Linux amd64, не multiarch. Локальный `docker build .` по-прежнему +собирает source target `runtime`; CI target использует уже проверенный binary. + +Workflow добавлен и проверен локально как конфигурация. Реальный GitHub-run, +Docker daemon и push в ваш Gitea здесь не выполнялись; для этого нужны репозиторий +и перечисленные настройки. CI намеренно останавливает публикацию при любой ошибке. + +Документация registry: https://docs.gitea.com/usage/packages/container/ diff --git a/docs/SOURCE-SHA256.txt b/docs/SOURCE-SHA256.txt index a6fb767..3ce5127 100644 --- a/docs/SOURCE-SHA256.txt +++ b/docs/SOURCE-SHA256.txt @@ -1,8 +1,15 @@ +9c4e46e42c549436c503a4cd70f90efd8f7754ce8c2103a6d1352cd34174aac0 .github/workflows/ci.yml 7136ea911bb6b6d352f703f80efeebde921028ea9dc39cd0d2cd55ea465f8c37 examples/config.json 8a65739eb5c24f10b9f2c509f0bd050fb1115706e6a41fdf804b9e0e44f583fd examples/config.toml ca86274bcf4dc5daebbf69c9f003854afca92954719aff068f810e0a3d615dea examples/config.yaml bbc2591581d382fef5fb918a480e6c338c7e3e3f2025df5509f51708534631e7 examples/converse-origin.fragment.yaml 66ea9e2b58f6a39b70911b9981cae5e99625483af7a41168e24923464de8ae56 examples/reverse-proxy.fragment.yaml +c08bd5923906f4d2163a258c8b68dbef39ea6a3e07032f67e5c12d3d9ca81c5e scripts/ci/image-tags.py +8820d262f8018a822b27eb9e5f0d3b9ca0c1272001164ca8a59fcd13911a5426 scripts/ci/prepare-minio.sh +5bcd646c6c9159bbdce3a4eadce4d80d6c7422748e47c6ad04e5769efb4f2920 scripts/ci/prepare-reference.sh +cb44f2692aefd618872153aed2658a7ab5c1a420802592e3240553b1f8da5ea5 scripts/ci/vendor/README.md +1e558d3d2211ea0228596aced2b71306503190c454ec4cb44f172045b3513c34 scripts/ci/vendor/prosody-cafd74b9c5ea.tar.gz +7ae11639143308ed4acd30ef6f99f55c7575893e5f6dce819d1f9d7da00cc29c scripts/ci/verify-all.sh 16e476e50c5802efcbb7d1aa68141c183c60f5ab17037d697dce720c209141f4 scripts/docker-entrypoint.sh 6fcb8a4977fee840d826951b0c30a3f2477c55d00860c79e99f635ca6e215e2b scripts/filter-android-pep-log.py a83cfdb6d910af1add9a14df7b0bd43fb0e2a23342f1a1eec831d47c659bba6f scripts/integration_http.py diff --git a/scripts/ci/image-tags.py b/scripts/ci/image-tags.py new file mode 100644 index 0000000..9d9ab1b --- /dev/null +++ b/scripts/ci/image-tags.py @@ -0,0 +1,44 @@ +#!/usr/bin/env python3 +"""Validate Gitea publication inputs and emit literal, shell-safe image tags.""" +import os +import re +import tomllib +from pathlib import Path + + +def image_tags(env, version): + registry = env.get('GITEA_REGISTRY', '') + image = env.get('GITEA_IMAGE', '') + if not re.fullmatch(r'[a-z0-9](?:[a-z0-9.-]*[a-z0-9])?(?::[0-9]{1,5})?', registry): + raise ValueError('GITEA_REGISTRY must be a lowercase hostname[:port], without scheme or path') + if not re.fullmatch(r'[a-z0-9]+(?:[._-][a-z0-9]+)*(?:/[a-z0-9]+(?:[._-][a-z0-9]+)*)+', image): + raise ValueError('GITEA_IMAGE must be lowercase owner/image, without registry or tag') + if not env.get('REGISTRY_USER') or not env.get('REGISTRY_TOKEN'): + raise ValueError('Set GITEA_REGISTRY_USERNAME and GITEA_REGISTRY_TOKEN secrets') + sha = env['GITHUB_SHA'] + if not re.fullmatch(r'[0-9a-f]{40}', sha): + raise ValueError('Invalid commit SHA') + base = f'{registry}/{image}' + tags = [f'{base}:sha-{sha}'] + ref = env['GITHUB_REF'] + if ref.startswith('refs/tags/'): + release = ref.removeprefix('refs/tags/') + if release != f'v{version}' or not re.fullmatch(r'v[0-9]+\.[0-9]+\.[0-9]+(?:-[0-9A-Za-z.-]+)?', release): + raise ValueError('Release tag must be v (no build metadata)') + tags.append(f'{base}:{release.lower()}') + elif ref == 'refs/heads/' + env['DEFAULT_BRANCH']: + tags.append(f'{base}:latest') + else: + raise ValueError('Only default branch or version tags may publish') + return tags + + +def main(): + version = tomllib.loads(Path('Cargo.toml').read_text())['package']['version'] + tags = image_tags(os.environ, version) + with open(os.environ['GITHUB_OUTPUT'], 'a', encoding='utf-8') as out: + out.write('tags<