name: publish # Build the Python package with uv and publish it to the self-hosted Gitea PyPI # registry on merge. dev -> a dev package (aegis-gitea-mcp-dev, .devN versions); # main -> the stable package (aegis-gitea-mcp). Gated on lint, tests and the security # checks so a release can never ship red. Reuses the REGISTRY_TOKEN package secret # (same one ci.yml uses); if it is absent the job fails loudly instead of publishing # anonymously. on: push: branches: - dev - main jobs: # --------------------------------------------------------------------------- # 1. Lint (same gate as ci.yml, via uv and the lockfile). # --------------------------------------------------------------------------- lint: runs-on: ubuntu-latest steps: - name: Checkout uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 with: persist-credentials: false - name: Set up Python uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 with: python-version: "3.12" - name: Set up uv uses: astral-sh/setup-uv@d4b2f3b6ecc6e67c4457f6d3e41ec42d3d0fcb86 # v5.4.2 with: version: "0.12.5" enable-cache: false - name: Install dependencies run: uv sync --locked - name: Run lint run: | uv run ruff check . uv run ruff format --check . uv run mypy src # --------------------------------------------------------------------------- # 2. Test: unit tests with the coverage gate from pyproject.toml. # --------------------------------------------------------------------------- test: runs-on: ubuntu-latest steps: - name: Checkout uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 with: persist-credentials: false - name: Set up Python uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 with: python-version: "3.12" - name: Set up uv uses: astral-sh/setup-uv@d4b2f3b6ecc6e67c4457f6d3e41ec42d3d0fcb86 # v5.4.2 with: version: "0.12.5" enable-cache: false - name: Install dependencies run: uv sync --locked - name: Run tests run: uv run pytest # --------------------------------------------------------------------------- # 3. Security: the same gates as ci.yml (secret scan over the history, security # tests, dependency audit, bandit rules). Nothing is published without them. # --------------------------------------------------------------------------- security: runs-on: ubuntu-latest steps: - name: Checkout uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 with: fetch-depth: 0 persist-credentials: false - name: Set up Python uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 with: python-version: "3.12" - name: Set up uv uses: astral-sh/setup-uv@d4b2f3b6ecc6e67c4457f6d3e41ec42d3d0fcb86 # v5.4.2 with: version: "0.12.5" enable-cache: false - name: Install dependencies run: uv sync --locked - name: Install gitleaks shell: bash run: | bin_dir="${RUNNER_TEMP:-/tmp}/gitleaks-bin" bash scripts/ci/install_gitleaks.sh "${bin_dir}" echo "${bin_dir}" >> "$GITHUB_PATH" - name: Scan history shell: bash run: gitleaks git --config .gitleaks.toml --redact --no-banner --verbose --exit-code 1 . - name: Security tests run: uv run pytest tests/security --no-cov - name: Dependency audit run: bash scripts/ci/pip_audit.sh - name: Ruff security rules run: uv run ruff check --select S . # --------------------------------------------------------------------------- # 4. Build with uv and publish to the Gitea PyPI registry. # # dev -> aegis-gitea-mcp-dev X.Y.Z.dev (always unique) # main -> aegis-gitea-mcp X.Y.Z (no-op if already there) # # The package name + version are patched into pyproject.toml at build time # only — never committed. The committed file keeps name "aegis-gitea-mcp" # and version "X.Y.Z". # --------------------------------------------------------------------------- publish: needs: [lint, test, security] runs-on: ubuntu-latest steps: - name: Checkout uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 with: persist-credentials: false - name: Set up Python uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 with: python-version: "3.12" - name: Set up uv uses: astral-sh/setup-uv@d4b2f3b6ecc6e67c4457f6d3e41ec42d3d0fcb86 # v5.4.2 with: version: "0.12.5" enable-cache: false - name: Require publish credentials shell: bash env: REGISTRY_TOKEN: ${{ secrets.REGISTRY_TOKEN }} run: | if [ -z "${REGISTRY_TOKEN}" ]; then echo "::error::REGISTRY_TOKEN secret is not set. Configure a PAT with write:package." >&2 exit 1 fi - name: Compute channel (package name + version) id: chan shell: bash run: | BASE="$(grep -E '^version = ' pyproject.toml | head -1 | sed -E 's/^version = "([^"]+)".*/\1/')" if [ "${GITHUB_REF_NAME}" = "main" ]; then PKG_NAME="aegis-gitea-mcp" PKG_VERSION="${BASE}" CHANNEL="stable" else PKG_NAME="aegis-gitea-mcp-dev" # Drop an existing .devN suffix (the committed version may be a # development version) so the result has exactly one dev segment. PKG_VERSION="${BASE%%.dev*}.dev${GITHUB_RUN_NUMBER}" CHANNEL="dev" fi echo "pkg_name=${PKG_NAME}" >> "$GITHUB_OUTPUT" echo "pkg_version=${PKG_VERSION}" >> "$GITHUB_OUTPUT" echo "channel=${CHANNEL}" >> "$GITHUB_OUTPUT" echo "Publishing ${PKG_NAME} ${PKG_VERSION} (${CHANNEL})" - name: Patch package name + version (build only, not committed) shell: bash run: | sed -i -E "s/^name = \".*\"/name = \"${{ steps.chan.outputs.pkg_name }}\"/" pyproject.toml sed -i -E "s/^version = \".*\"/version = \"${{ steps.chan.outputs.pkg_version }}\"/" pyproject.toml echo "--- patched [project] header ---" grep -E '^(name|version) = ' pyproject.toml - name: Build sdist + wheel shell: bash run: uv build - name: Upload build artifacts # Best-effort: some Gitea act_runner versions don't fully support the # v4 artifact backend. The real deliverable is published to the registry # below, so a failed artifact upload must not fail the release. continue-on-error: true uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2 with: name: dist-${{ steps.chan.outputs.channel }} path: dist/* - name: Publish to Gitea PyPI registry shell: bash env: # Reuse the existing package secret (same one ci.yml uses). The # token authenticates as its owning Gitea user, so the actor is the # username and the token is the password. uv reads the credentials from these variables, so the token never # appears on a command line (process listings, shell traces). UV_PUBLISH_USERNAME: ${{ github.actor }} UV_PUBLISH_PASSWORD: ${{ secrets.REGISTRY_TOKEN }} run: | # --check-url makes uv skip files already in the registry, so a main # push that did not bump the version is a clean no-op instead of a 409. uv publish \ --publish-url https://git.hiddenden.cafe/api/packages/Hiddenden/pypi \ --check-url https://git.hiddenden.cafe/api/packages/Hiddenden/pypi/simple/ # Optional second step to also publish to public PyPI lives behind its own # secret. Intentionally left as a disabled stub: this pass does NOT push # to public PyPI. # # - name: Publish to public PyPI # if: ${{ secrets.PYPI_TOKEN != '' }} # shell: bash # env: # UV_PUBLISH_USERNAME: __token__ # UV_PUBLISH_PASSWORD: ${{ secrets.PYPI_TOKEN }} # run: uv publish