diff --git a/.github/actionlint-matcher.json b/.github/actionlint-matcher.json new file mode 100644 index 0000000000..4613e1617b --- /dev/null +++ b/.github/actionlint-matcher.json @@ -0,0 +1,17 @@ +{ + "problemMatcher": [ + { + "owner": "actionlint", + "pattern": [ + { + "regexp": "^(?:\\x1b\\[\\d+m)?(.+?)(?:\\x1b\\[\\d+m)*:(?:\\x1b\\[\\d+m)*(\\d+)(?:\\x1b\\[\\d+m)*:(?:\\x1b\\[\\d+m)*(\\d+)(?:\\x1b\\[\\d+m)*: (?:\\x1b\\[\\d+m)*(.+?)(?:\\x1b\\[\\d+m)* \\[(.+?)\\]$", + "file": 1, + "line": 2, + "column": 3, + "message": 4, + "code": 5 + } + ] + } + ] +} diff --git a/.github/actionlint-sarif-template.txt b/.github/actionlint-sarif-template.txt new file mode 100644 index 0000000000..01c589e218 --- /dev/null +++ b/.github/actionlint-sarif-template.txt @@ -0,0 +1,63 @@ +{ + "$schema": "https://json.schemastore.org/sarif-2.1.0.json", + "version": "2.1.0", + "runs": [ + { + "tool": { + "driver": { + "name": "Actionlint", + "version": {{ getVersion | json }}, + "informationUri": "https://github.com/rhysd/actionlint", + "rules": [ + {{$first := true}} + {{range $ := allKinds}} + {{if $first}}{{$first = false}}{{else}},{{end}} + { + "id": {{json $.Name}}, + "name": {{$.Name | toPascalCase | json}}, + "defaultConfiguration": { + "level": "warning" + }, + "fullDescription": { + "text": {{json $.Description}} + }, + "helpUri": "https://github.com/rhysd/actionlint/blob/v1.7.12/docs/checks.md" + } + {{end}} + ] + } + }, + "results": [ + {{$first := true}} + {{range $ := .}} + {{if $first}}{{$first = false}}{{else}},{{end}} + { + "ruleId": {{json $.Kind}}, + "level": "warning", + "message": { + "text": {{json $.Message}} + }, + "locations": [ + { + "physicalLocation": { + "artifactLocation": { + "uri": {{json $.Filepath}}, + "uriBaseId": "%SRCROOT%" + }, + "region": { + "startLine": {{$.Line}}, + "startColumn": {{$.Column}}, + "endColumn": {{$.EndColumn}}, + "snippet": { + "text": {{json $.Snippet}} + } + } + } + } + ] + } + {{end}} + ] + } + ] +} diff --git a/.github/actionlint.yml b/.github/actionlint.yml new file mode 100644 index 0000000000..35aa72ea78 --- /dev/null +++ b/.github/actionlint.yml @@ -0,0 +1,18 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 + +self-hosted-runner: + labels: + - linux-amd64-cpu8 + - linux-amd64-gpu-rtxpro6000-latest-1 + - linux-arm64-cpu8 + - linux-arm64-gpu-l4-latest-1 + - nv + - ubuntu-26.04 + - windows-arm64 + - wsl-amd64-gpu-rtxpro6000-latest-1 + +paths: + .github/workflows/windows-msvc.yml: + ignore: + - 'constant expression "false" in condition' diff --git a/.github/actions/setup-nix/action.yml b/.github/actions/setup-nix/action.yml new file mode 100644 index 0000000000..3cd6940c05 --- /dev/null +++ b/.github/actions/setup-nix/action.yml @@ -0,0 +1,24 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 + +name: Setup Nix +description: Install Nix and configure the OpenShell Cachix cache + +inputs: + cachix-auth-token: + description: Token used to write build outputs to Cachix + required: false + default: "" + +runs: + using: composite + steps: + - uses: cachix/install-nix-action@13d8dd58da0234aa297dedd986986ccb8e7f3e24 # v31.11.1 + with: + github_access_token: ${{ github.token }} + + - uses: cachix/cachix-action@5f2d7c5294214f71b873db4b969586b980625e71 # v17 + with: + name: openshell + authToken: ${{ inputs.cachix-auth-token }} + skipPush: ${{ inputs.cachix-auth-token == '' }} diff --git a/.github/codeql/codeql-config.yml b/.github/codeql/codeql-config.yml new file mode 100644 index 0000000000..3953988b89 --- /dev/null +++ b/.github/codeql/codeql-config.yml @@ -0,0 +1,15 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 + +name: OpenShell Rust and SDKs + +paths: + - crates + - examples + - sdk/go + - sdk/typescript/src + - python/openshell + +paths-ignore: + - python/openshell/_proto + - sdk/typescript/src/gen diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml new file mode 100644 index 0000000000..28057fe18a --- /dev/null +++ b/.github/workflows/codeql.yml @@ -0,0 +1,128 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 + +name: CodeQL + +on: + pull_request: + merge_group: + types: [checks_requested] + push: + branches: [main] + schedule: + - cron: "29 5 * * 6" + workflow_dispatch: + +permissions: + contents: read + security-events: write + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +jobs: + analyze: + name: CodeQL (${{ matrix.language }}) + runs-on: ubuntu-latest + timeout-minutes: 90 + strategy: + fail-fast: false + matrix: + include: + - language: rust + build-mode: none + - language: go + build-mode: manual + - language: python + build-mode: none + - language: javascript-typescript + build-mode: none + steps: + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + persist-credentials: false + + - name: Set up Go + if: matrix.language == 'go' + uses: actions/setup-go@b7ad1dad31e06c5925ef5d2fc7ad053ef454303e # v7.0.0 + with: + go-version-file: sdk/go/go.mod + cache-dependency-path: sdk/go/go.sum + + - name: Initialize CodeQL + uses: github/codeql-action/init@ff2f1c621b7f889edc0d3c761ac2e6a3f8cdb0dd # v4.37.7 + with: + languages: ${{ matrix.language }} + build-mode: ${{ matrix.build-mode }} + config-file: ./.github/codeql/codeql-config.yml + + - name: Build Go SDK + if: matrix.language == 'go' + working-directory: sdk/go + run: go build ./... + + - name: Analyze + id: analyze + uses: github/codeql-action/analyze@ff2f1c621b7f889edc0d3c761ac2e6a3f8cdb0dd # v4.37.7 + with: + category: /language:${{ matrix.language }} + output: codeql-results + upload: never + + - name: Summarize findings + if: always() + env: + LANGUAGE: ${{ matrix.language }} + shell: bash + run: | + set -euo pipefail + shopt -s globstar nullglob + sarif_files=(codeql-results/**/*.sarif) + + { + echo "### CodeQL: $LANGUAGE" + echo + if [ "${#sarif_files[@]}" -eq 0 ]; then + echo "No SARIF report was produced." + else + finding_count=$(jq -s '[.[].runs[]?.results[]?] | length' "${sarif_files[@]}") + echo "Findings: $finding_count" + echo + echo "Findings are informational and do not fail CI." + fi + } >> "$GITHUB_STEP_SUMMARY" + + - name: Upload SARIF to Code Scanning + if: steps.analyze.outcome == 'success' + uses: github/codeql-action/upload-sarif@ff2f1c621b7f889edc0d3c761ac2e6a3f8cdb0dd # v4.37.7 + with: + sarif_file: codeql-results + category: /language:${{ matrix.language }} + + - name: Upload SARIF + if: always() + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 + with: + name: codeql-${{ matrix.language }}-${{ github.run_id }} + path: codeql-results + if-no-files-found: ignore + retention-days: 14 + + result: + name: OpenShell / CodeQL (informational) + if: always() + needs: analyze + runs-on: ubuntu-latest + permissions: {} + steps: + - name: Evaluate analyzer execution + env: + ANALYZE_RESULT: ${{ needs.analyze.result }} + shell: bash + run: | + if [ "$ANALYZE_RESULT" != "success" ]; then + echo "::error::One or more CodeQL analyzers did not complete successfully." + exit 1 + fi + echo "All CodeQL analyzers completed; findings remain informational." diff --git a/.github/workflows/dependency-review.yml b/.github/workflows/dependency-review.yml new file mode 100644 index 0000000000..fc162fd2cb --- /dev/null +++ b/.github/workflows/dependency-review.yml @@ -0,0 +1,98 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 + +name: Dependency Review + +on: + pull_request: + merge_group: + types: [checks_requested] + workflow_dispatch: + inputs: + base_sha: + description: Base commit SHA to compare + required: true + type: string + head_sha: + description: Head commit SHA to compare + required: true + type: string + +permissions: + contents: read + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +jobs: + review: + name: Dependency Review (informational) + runs-on: ubuntu-latest + env: + BASE_REF: ${{ github.event.pull_request.base.sha || github.event.merge_group.base_sha || inputs.base_sha }} + HEAD_REF: ${{ github.event.pull_request.head.sha || github.event.merge_group.head_sha || inputs.head_sha }} + steps: + - name: Check Dependency Graph availability + id: preflight + uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9 + with: + script: | + const shaPattern = /^(?:[0-9a-f]{40}|[0-9a-f]{64})$/i; + const baseRef = process.env.BASE_REF; + const headRef = process.env.HEAD_REF; + + if (!shaPattern.test(baseRef) || !shaPattern.test(headRef)) { + core.setFailed("Dependency Review requires base and head commit SHAs."); + return; + } + + try { + await github.request( + "GET /repos/{owner}/{repo}/dependency-graph/compare/{basehead}", + { + owner: context.repo.owner, + repo: context.repo.repo, + basehead: `${baseRef}...${headRef}`, + headers: { + "X-GitHub-Api-Version": "2022-11-28", + }, + }, + ); + core.setOutput("available", "true"); + } catch (error) { + const status = error.status; + if (status === 403 || status === 404) { + core.setOutput("available", "false"); + core.warning( + `GitHub Dependency Graph is unavailable (HTTP ${status}); Dependency Review is skipped.`, + ); + await core.summary + .addHeading("Dependency Review", 3) + .addRaw(`GitHub Dependency Graph is unavailable (HTTP ${status}).`, true) + .addRaw( + "The informational review will start automatically once the repository feature is available.", + true, + ) + .write(); + return; + } + + const statusSuffix = status ? ` with HTTP ${status}` : ""; + core.setFailed( + `Dependency Graph preflight failed${statusSuffix}: ${error.message}`, + ); + } + + - name: Review dependency changes + if: steps.preflight.outputs.available == 'true' + uses: actions/dependency-review-action@a1d282b36b6f3519aa1f3fc636f609c47dddb294 # v5.0.0 + with: + base-ref: ${{ env.BASE_REF }} + head-ref: ${{ env.HEAD_REF }} + fail-on-severity: high + fail-on-scopes: runtime, development, unknown + warn-only: true + comment-summary-in-pr: never + license-check: false + show-openssf-scorecard: false diff --git a/.github/workflows/e2e-test.yml b/.github/workflows/e2e-test.yml index f176953048..d3e4230636 100644 --- a/.github/workflows/e2e-test.yml +++ b/.github/workflows/e2e-test.yml @@ -129,188 +129,6 @@ jobs: OPENSHELL_MCP_CONFORMANCE_CLIENT_IMAGE: ${{ format('openshell-mcp-conformance-client:{0}', inputs.image-tag) }} run: ${{ matrix.cmd }} - e2e-podman-rootless: - name: E2E (rust-podman-${{ matrix.suite }}, ${{ matrix.runner }}) - # Run directly on the Ubuntu host so the test observes the host's AppArmor - # and unprivileged-user-namespace policy. A privileged job container masks - # the restrictions that production rootless Podman installations enforce. - # Ubuntu 26.04 provides the supported Podman 5.x and pasta combination. - # Re-add older/slirp4netns environments when direct callbacks through a - # rootless-network namespace relay are supported. - runs-on: ${{ matrix.runner }} - timeout-minutes: 30 - strategy: - fail-fast: false - matrix: - include: - # Keep package versions explicit so hosted-runner tool overrides - # cannot silently change the supported test environment. - - suite: rootless - runner: ubuntu-26.04 - podman_major: "5" - podman_package_version: "5.7.0+ds2-3build1" - conmon_package_version: "2.1.13+ds1-2" - cmd: "mise run --no-deps --skip-deps e2e:podman:rootless" - - suite: external-driver - runner: ubuntu-26.04 - podman_major: "5" - podman_package_version: "5.7.0+ds2-3build1" - conmon_package_version: "2.1.13+ds1-2" - cmd: "env -u OPENSHELL_GATEWAY_BIN mise run --no-deps --skip-deps e2e:podman:external-driver" - - suite: provider-refresh-keycloak - runner: ubuntu-26.04 - podman_major: "5" - podman_package_version: "5.7.0+ds2-3build1" - conmon_package_version: "2.1.13+ds1-2" - cmd: "mise run --no-deps --skip-deps e2e:provider-refresh-keycloak" - env: - IMAGE_TAG: ${{ inputs.image-tag }} - MISE_GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - OPENSHELL_REGISTRY: ghcr.io/nvidia/openshell - OPENSHELL_REGISTRY_HOST: ghcr.io - OPENSHELL_REGISTRY_NAMESPACE: nvidia/openshell - OPENSHELL_REGISTRY_USERNAME: ${{ github.actor }} - OPENSHELL_REGISTRY_PASSWORD: ${{ secrets.GITHUB_TOKEN }} - OPENSHELL_SUPERVISOR_IMAGE: ${{ format('ghcr.io/nvidia/openshell/supervisor:{0}', inputs.image-tag) }} - steps: - - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - with: - ref: ${{ inputs['checkout-ref'] || github.sha }} - persist-credentials: false - - - name: Use prebuilt OpenShell CLI - if: inputs.cli-artifact-prefix != '' - uses: ./.github/actions/setup-e2e-cli - with: - artifact-prefix: ${{ inputs.cli-artifact-prefix }} - - - name: Use prebuilt OpenShell gateway - if: inputs.gateway-artifact-prefix != '' - uses: ./.github/actions/setup-e2e-gateway - with: - artifact-prefix: ${{ inputs.gateway-artifact-prefix }} - - - name: Install mise - run: | - curl https://mise.run | MISE_VERSION=v2026.4.25 sh - echo "$HOME/.local/bin" >> "$GITHUB_PATH" - echo "$HOME/.local/share/mise/shims" >> "$GITHUB_PATH" - - - name: Install tools - run: mise install --locked - - - name: Install Podman and build dependencies - run: | - sudo apt-get update - sudo apt-get install -y --no-install-recommends \ - apparmor \ - build-essential \ - clang \ - fuse-overlayfs \ - libssl-dev \ - libz3-dev \ - openssh-client \ - passt \ - pkg-config \ - "conmon=${{ matrix.conmon_package_version }}" \ - "podman=${{ matrix.podman_package_version }}" \ - uidmap - # Hosted runners can place newer Podman and conmon binaries under - # /usr/local ahead of Ubuntu's packages. Select the distro CLI and - # use Podman's supported final config override for its conmon path. - podman_config="${RUNNER_TEMP}/openshell-containers.conf" - printf '%s\n' \ - '[engine]' \ - 'conmon_path = ["/usr/bin/conmon"]' \ - > "${podman_config}" - echo "/usr/bin" >> "${GITHUB_PATH}" - echo "CONTAINERS_CONF_OVERRIDE=${podman_config}" >> "${GITHUB_ENV}" - - - name: Allow pasta to receive Podman stop signals - # Ubuntu's packaged pasta profile currently blocks this signal, forcing - # Podman to wait for its SIGKILL fallback. Keep this narrow allowance - # until the distribution package includes the upstream profile fix. - run: | - set -euo pipefail - profile=/etc/apparmor.d/usr.bin.pasta - rule=' signal (receive) peer=podman,' - if ! sudo grep -Fqx "${rule}" "${profile}"; then - sudo sed -i '\|^ include $|a\ signal (receive) peer=podman,' "${profile}" - fi - sudo grep -Fqx "${rule}" "${profile}" - sudo apparmor_parser --replace "${profile}" - - - name: Configure rootless Podman - run: | - set -euo pipefail - if ! grep -q "^${USER}:" /etc/subuid; then - sudo usermod --add-subuids 100000-165535 "$USER" - fi - if ! grep -q "^${USER}:" /etc/subgid; then - sudo usermod --add-subgids 100000-165535 "$USER" - fi - runtime_dir="/run/user/$(id -u)" - sudo install -d -m 0700 -o "$(id -u)" -g "$(id -g)" "$runtime_dir" - echo "XDG_RUNTIME_DIR=$runtime_dir" >> "$GITHUB_ENV" - - - name: Verify rootless Podman environment - run: | - set -euo pipefail - podman_version="$(podman version --format '{{.Client.Version}}')" - case "$podman_version" in - "${{ matrix.podman_major }}".*) ;; - *) echo "ERROR: expected Podman ${{ matrix.podman_major }}.x, found $podman_version" >&2; exit 1 ;; - esac - test "$(dpkg-query -W -f='${Version}' podman)" = "${{ matrix.podman_package_version }}" - test "$(dpkg-query -W -f='${Version}' conmon)" = "${{ matrix.conmon_package_version }}" - test "$(command -v podman)" = "/usr/bin/podman" - test "$(podman info --format '{{.Host.Conmon.Path}}')" = "/usr/bin/conmon" - test "$(podman info --format '{{.Host.Security.Rootless}}')" = "true" - test "$(podman info --format '{{.Host.RootlessNetworkCmd}}')" = "pasta" - test "$(sudo sysctl -n kernel.apparmor_restrict_unprivileged_userns)" = "1" - echo "=== host ===" - uname -a - echo "=== AppArmor ===" - cat /proc/self/attr/current - sudo aa-status || true - echo "=== Podman ===" - podman version - podman info --debug - - - name: Probe rootless capability bounding set - run: | - set -euo pipefail - probe="$RUNNER_TEMP/openshell-capbset-probe" - cc -static -O2 -Wall -Wextra -Werror \ - e2e/support/capbset-probe.c \ - -o "$probe" - podman run --rm \ - --cap-add=SETPCAP \ - --volume "$probe:/openshell-capbset-probe:ro" \ - docker.io/library/alpine:3.22 \ - /openshell-capbset-probe - - - name: Log in to GHCR with Podman - run: echo "${{ secrets.GITHUB_TOKEN }}" | podman login ghcr.io -u "${{ github.actor }}" --password-stdin - - - name: Run Podman E2E - run: ${{ matrix.cmd }} - - - name: Print AppArmor denials - if: always() - run: sudo dmesg | grep -E 'apparmor=.*DENIED|profile="unprivileged_userns"' | tail -100 || true - - - name: Fail on pasta SIGTERM AppArmor denial - if: always() - run: | - set -euo pipefail - denials="$(sudo dmesg | grep -E 'profile="pasta".*requested_mask="receive".*signal=term.*peer="podman"' || true)" - if [ -n "${denials}" ]; then - echo "::error::pasta denied Podman's SIGTERM; Podman will use its SIGKILL fallback" - printf '%s\n' "${denials}" - exit 1 - fi - e2e-vm: name: E2E (rust-vm-${{ matrix.suite }}) # libkrun needs KVM, so this job must run directly on a GitHub-hosted diff --git a/.github/workflows/stale.yml b/.github/workflows/stale.yml index bfdc13af8f..e45ccbf5eb 100644 --- a/.github/workflows/stale.yml +++ b/.github/workflows/stale.yml @@ -9,21 +9,22 @@ permissions: contents: read jobs: - stale: + stale-issues: runs-on: ubuntu-latest permissions: issues: write - pull-requests: write steps: - uses: actions/stale@4391f3da665fdf50b6810c1a66712fb9ba21aa93 # v11.0.0 with: stale-issue-label: state:stale - stale-pr-label: state:stale days-before-issue-stale: 14 days-before-issue-close: -1 # -1 puts this into dry-run mode. Update to 7 to enable closing. - days-before-pr-stale: 14 - days-before-pr-close: -1 # -1 puts this into dry-run mode. Update to 7 to enable closing. + days-before-pr-stale: -1 + days-before-pr-close: -1 + + operations-per-run: 300 + sort-by: updated exempt-issue-labels: state:triage-needed,state:validated,state:accepted,agent:plan-requested,agent:plan-ready,agent:implementation-requested,agent:in-progress,agent:pr-opened,roadmap close-issue-reason: not_planned @@ -35,6 +36,24 @@ jobs: close-issue-message: > Closing this issue after 7 days with no activity since it was marked stale. Reopen it if the work is still relevant. + stale-pull-requests: + runs-on: ubuntu-latest + permissions: + issues: write + pull-requests: write + steps: + - uses: actions/stale@4391f3da665fdf50b6810c1a66712fb9ba21aa93 # v11.0.0 + with: + stale-pr-label: state:stale + + days-before-issue-stale: -1 + days-before-issue-close: -1 + days-before-pr-stale: 14 + days-before-pr-close: -1 # -1 puts this into dry-run mode. Update to 7 to enable closing. + + operations-per-run: 300 + sort-by: updated + stale-pr-message: > This pull request has had no activity for 14 days and is now marked stale. It may be closed in 7 days if there is no further activity. diff --git a/.github/workflows/workflow-security.yml b/.github/workflows/workflow-security.yml new file mode 100644 index 0000000000..7a87e15483 --- /dev/null +++ b/.github/workflows/workflow-security.yml @@ -0,0 +1,185 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 + +name: Workflow Security Reports + +on: + pull_request: + merge_group: + types: [checks_requested] + push: + branches: [main] + schedule: + - cron: "17 6 * * 1" + workflow_dispatch: + +permissions: + contents: read + +defaults: + run: + shell: nix develop --command bash -euo pipefail {0} + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +jobs: + actionlint: + name: Actionlint (informational) + runs-on: ubuntu-latest + permissions: + contents: read + security-events: write + steps: + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + persist-credentials: false + + - name: Set up Nix + uses: ./.github/actions/setup-nix + with: + cachix-auth-token: ${{ github.event_name != 'pull_request' && secrets.CACHIX_AUTH_TOKEN || '' }} + + - name: Run Actionlint + run: | + set -uo pipefail + mkdir -p reports + echo "::add-matcher::.github/actionlint-matcher.json" + + set +e + actionlint -shellcheck= -pyflakes= 2>&1 | tee reports/actionlint.txt + status=${PIPESTATUS[0]} + + echo "::remove-matcher owner=actionlint::" + actionlint \ + -shellcheck= \ + -pyflakes= \ + -format "$(cat .github/actionlint-sarif-template.txt)" \ + > reports/actionlint.sarif + sarif_status=$? + set -e + + if [ "$status" -le 1 ] && [ "$sarif_status" -ne "$status" ]; then + echo "::error::Actionlint could not produce SARIF (exit $sarif_status)." + exit "$sarif_status" + fi + + { + echo "### Actionlint" + echo + } >> "$GITHUB_STEP_SUMMARY" + + case "$status" in + 0) + echo "No findings." >> "$GITHUB_STEP_SUMMARY" + ;; + 1) + echo "::warning::Actionlint reported findings; this check is informational." + echo "Findings were reported as annotations and do not fail CI." >> "$GITHUB_STEP_SUMMARY" + ;; + 2|3) + echo "::error::Actionlint could not complete (exit $status)." + echo "Actionlint failed to run correctly (exit $status)." >> "$GITHUB_STEP_SUMMARY" + exit "$status" + ;; + *) + echo "::error::Actionlint returned unexpected exit code $status." + echo "Actionlint returned unexpected exit code $status." >> "$GITHUB_STEP_SUMMARY" + exit "$status" + ;; + esac + + - name: Upload Actionlint SARIF to Code Scanning + if: success() + uses: github/codeql-action/upload-sarif@ff2f1c621b7f889edc0d3c761ac2e6a3f8cdb0dd # v4.37.7 + with: + sarif_file: reports/actionlint.sarif + category: actionlint + + - name: Upload Actionlint report + if: always() + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 + with: + name: actionlint-${{ github.run_id }} + path: | + reports/actionlint.txt + reports/actionlint.sarif + if-no-files-found: ignore + retention-days: 14 + + zizmor: + name: Zizmor High report (informational) + runs-on: ubuntu-latest + permissions: + contents: read + security-events: write + steps: + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + persist-credentials: false + + - name: Set up Nix + uses: ./.github/actions/setup-nix + with: + cachix-auth-token: ${{ github.event_name != 'pull_request' && secrets.CACHIX_AUTH_TOKEN || '' }} + + - name: Run Zizmor + run: | + set -euo pipefail + mkdir -p reports + + zizmor \ + --offline \ + --persona=regular \ + --min-severity=high \ + --no-exit-codes \ + --format=json \ + . > reports/zizmor-high.json + + zizmor \ + --offline \ + --persona=regular \ + --min-severity=high \ + --no-exit-codes \ + --format=sarif \ + . > reports/zizmor-high.sarif + + finding_count=$(jq 'length' reports/zizmor-high.json) + { + echo "### Zizmor high-severity report" + echo + echo "Zizmor has no critical severity; high is its maximum level." + echo + echo "Findings: $finding_count" + if [ "$finding_count" -gt 0 ]; then + echo + jq -r \ + 'group_by(.ident) | .[] | "- `\(.[0].ident)`: \(length)"' \ + reports/zizmor-high.json + echo + echo "These findings are informational and do not fail CI." + fi + } >> "$GITHUB_STEP_SUMMARY" + + if [ "$finding_count" -gt 0 ]; then + echo "::warning::Zizmor reported $finding_count high-severity findings; this check is informational." + fi + + - name: Upload Zizmor SARIF to Code Scanning + if: success() + uses: github/codeql-action/upload-sarif@ff2f1c621b7f889edc0d3c761ac2e6a3f8cdb0dd # v4.37.7 + with: + sarif_file: reports/zizmor-high.sarif + category: zizmor-high + + - name: Upload Zizmor reports + if: always() + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 + with: + name: zizmor-high-${{ github.run_id }} + path: | + reports/zizmor-high.json + reports/zizmor-high.sarif + if-no-files-found: ignore + retention-days: 14 diff --git a/.github/zizmor.yml b/.github/zizmor.yml new file mode 100644 index 0000000000..7b1e035b77 --- /dev/null +++ b/.github/zizmor.yml @@ -0,0 +1,13 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 + +rules: + dangerous-triggers: + ignore: + # These base-branch workflows never check out or execute pull request + # head code. Keep each suppression scoped to its reviewed trigger block. + - dco.yml:3 + - e2e-label-help.yml:13 + - release-canary.yml:3 + - required-ci-gates.yml:3 + - vouch-check.yml:3 diff --git a/CI.md b/CI.md index aae22f4c4f..c3d33622f7 100644 --- a/CI.md +++ b/CI.md @@ -14,7 +14,7 @@ Merge queue validation is a second integration gate for `main`. After a PR has p Three opt-in labels enable the long-running E2E suites: -- `test:e2e` runs the standard Docker, rootless Podman, Kubernetes, and VM E2E +- `test:e2e` runs the standard Docker, Kubernetes, and VM E2E suites in `Branch E2E Checks` - `test:e2e-gpu` runs GPU E2E in `Branch E2E Checks` - `test:e2e-kubernetes` runs Kubernetes E2E with the HA Helm overlay @@ -26,6 +26,41 @@ The `OpenShell / E2E` and `OpenShell / GPU E2E` required statuses are evaluated The GitHub ruleset should require the `OpenShell / ...` statuses published by `Required CI Gates`, not the push-triggered workflow jobs directly. +## Informational security reports + +Security analysis that does not need NVIDIA infrastructure runs directly on +GitHub-hosted runners. These workflows receive no secrets and run on fork pull +requests without waiting for copy-pr-bot. Scanner jobs request +`security-events: write` to publish SARIF to Code Scanning. GitHub permits +Code Scanning uploads from `pull_request` runs even when fork and Dependabot +contexts receive a read-only `GITHUB_TOKEN`, so each scanner uploads results +directly and also retains report artifacts: + +- `Workflow Security Reports` runs Actionlint and Zizmor. Actionlint reports + workflow syntax and expression findings. Zizmor reports only High severity, + its maximum level. Nix provides both scanners. They publish SARIF to Code + Scanning and retain report artifacts. +- `Dependency Review` compares the base and head dependency graphs and reports + newly introduced vulnerabilities with a High-or-higher policy. It runs in + warn-only mode. A preflight turns an unavailable GitHub Dependency Graph into + a warning, so the workflow remains neutral until the repository feature is + available. +- `CodeQL` analyzes product Rust code, examples, and the Go, Python, and + TypeScript SDKs. E2E test code is excluded. Results are uploaded to Code + Scanning and always retained as workflow artifacts. + +Findings do not fail these workflows. Tool startup, configuration, build, and +analysis failures still fail so a broken scanner cannot appear healthy. These +workflows also run on merge groups, but their checks are not required statuses +and do not gate merges. + +Run the workflow-definition scanners locally with: + +```shell +nix develop --command actionlint -shellcheck= -pyflakes= +nix develop --command zizmor --offline --persona=regular --min-severity=high --no-exit-codes . +``` + ## Commit signing copy-pr-bot decides whether to mirror a PR automatically based on whether the author is trusted. For org members and collaborators, "trusted" means **all commits in the PR are cryptographically signed**. Unsigned commits, even from an org member, force the bot to wait for a maintainer's `/ok to test `. @@ -138,10 +173,14 @@ The bot's full administrator documentation is internal to NVIDIA. The only comma | `.github/workflows/branch-checks.yml` | Required non-E2E checks. Triggers on `push: pull-request/[0-9]+` for PR mirrors and `merge_group` for queued merges. | | `.github/workflows/branch-e2e.yml` | Standard, GPU, Kubernetes HA, and Kubernetes credential-driver E2E. PR mirror pushes use `test:e2e`, `test:e2e-gpu`, and `test:e2e-kubernetes` labels; merge groups run core and GPU E2E. | | `.github/workflows/helm-lint.yml` | Helm chart validation. PR mirror pushes skip lint jobs unless Helm inputs changed; merge groups always validate Helm because they represent the final integration state. | +| `.github/actions/setup-nix/action.yml` | Installs Nix and configures the OpenShell Cachix cache, using read-only cache access when no authentication token is available. | | `.github/actions/pr-gate/action.yml` | Composite action that resolves PR metadata and verifies the required label is set for PR mirror pushes. Non-push events are allowed through. | | `.github/actions/pr-merge-base/action.yml` | Composite action that resolves and fetches the merge-base commit for `pull-request/` push workflows. | | `.github/workflows/required-ci-gates.yml` | Posts required PR-head and merge-group statuses for gated CI workflows. This is what branch protection and merge queue should require. | | `.github/workflows/e2e-label-help.yml` | When a `test:e2e*` label is applied, posts a PR comment telling the maintainer the next manual step (re-run an existing workflow run, or `/ok to test ` to refresh the mirror). | +| `.github/workflows/workflow-security.yml` | Runs informational Actionlint and High-severity Zizmor reports on GitHub-hosted runners. | +| `.github/workflows/dependency-review.yml` | Reports dependency changes when GitHub Dependency Graph is available; otherwise publishes a neutral warning. | +| `.github/workflows/codeql.yml` | Runs informational CodeQL analysis for Rust and the Go, Python, and TypeScript SDKs and retains SARIF artifacts. | ## Release workflows @@ -163,3 +202,6 @@ Require these statuses in the branch ruleset for PR and merge-queue CI: - `OpenShell / Helm Lint` Do not require the underlying workflow jobs directly. PR workflow jobs only appear after copy-pr-bot mirrors trusted code, and merge-group workflow jobs run on temporary queue branches. The stable `OpenShell / ...` contexts prove the expected workflow completed for the commit that GitHub is about to merge. + +Do not add the informational Actionlint, Zizmor, Dependency Review, or CodeQL +jobs to the required status list while they remain in observation mode. diff --git a/Cargo.lock b/Cargo.lock index 207535ab10..011730760d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2118,11 +2118,9 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "899def5c37c4fd7b2664648c28120ecec138e4d395b459e5ca34f9cce2dd77fd" dependencies = [ "cfg-if", - "js-sys", "libc", "r-efi 5.3.0", "wasip2", - "wasm-bindgen", ] [[package]] @@ -5371,15 +5369,16 @@ dependencies = [ [[package]] name = "quinn-proto" -version = "0.11.14" +version = "0.11.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "434b42fec591c96ef50e21e886936e66d3cc3f737104fdb9b737c40ffb94c098" +checksum = "04759210543be93709136e28212294a659ef5001836ff4eab4d663e4529bba83" dependencies = [ "aws-lc-rs", "bytes", - "getrandom 0.3.4", + "getrandom 0.4.2", "lru-slab", - "rand 0.9.4", + "rand 0.10.2", + "rand_pcg", "ring", "rustc-hash 2.1.2", "rustls 0.23.38", @@ -5502,6 +5501,15 @@ version = "0.10.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "63b8176103e19a2643978565ca18b50549f6101881c443590420e4dc998a3c69" +[[package]] +name = "rand_pcg" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "caa0f4137e1c0a72f4c651489402276c8e8e1cf081f3b0ba156d2cbeef09e86a" +dependencies = [ + "rand_core 0.10.1", +] + [[package]] name = "rand_xoshiro" version = "0.7.0" diff --git a/architecture/build.md b/architecture/build.md index 7daf4a7c66..a1c6974989 100644 --- a/architecture/build.md +++ b/architecture/build.md @@ -272,6 +272,45 @@ Repository CI keeps telemetry compiled into release-parity artifacts but disables emission for Rust tests, E2E runs, and release canaries. This prevents synthetic activity from contributing to product usage metrics. +Static security checks are deliberately outside the mirror-branch path. They run +directly on GitHub-hosted runners with no secrets, so they also cover fork pull +requests and consume no NVIDIA self-hosted capacity. Scanner jobs request +`security-events: write` and upload SARIF to Code Scanning directly on every +event they run on, including fork and Dependabot pull requests, which Code +Scanning permits for `pull_request` runs despite their read-only `GITHUB_TOKEN`. +Each scanner also retains its report as a workflow artifact. No privileged +intermediate workflow relays those uploads. +Triggers differ by workflow: `.github/workflows/workflow-security.yml` and +`.github/workflows/codeql.yml` run on `pull_request`, `merge_group`, `main`, and +a weekly schedule; `.github/workflows/dependency-review.yml` runs on +`pull_request` and `merge_group` only, because it needs a base and head commit +to compare. + +- **Actionlint and Zizmor** analyze the workflow definitions themselves. + Repository configuration lives in `.github/actionlint.yml` (self-hosted runner + labels, scoped per-file ignores) and `.github/zizmor.yml` (scoped rule + suppressions). Zizmor runs offline and reports only High severity, which is + its maximum level. Both publish SARIF to Code Scanning and retain report + artifacts. The Nix flake provides both scanners, so local runs use + `nix develop --command actionlint -shellcheck= -pyflakes=` and + `nix develop --command zizmor --offline --persona=regular --min-severity=high --no-exit-codes .`. +- **Dependency Review** compares the base and head dependency graphs. It + preflights the GitHub Dependency Graph compare API and neutralizes itself with + a warning while that repository feature is unavailable, so the check begins + reporting on its own once the feature is enabled. Reviews run in warn-only + mode. +- **CodeQL** analyzes product Rust code, examples, and the Go, Python, and + TypeScript SDKs, scoped by `.github/codeql/codeql-config.yml`; E2E test code is + excluded. Only Go requires a build; the other languages use build mode `none`. + Results are uploaded to Code Scanning and always retained as workflow + artifacts. + +Findings never fail these checks; scanner and build failures do. A scanner that +cannot run, a CodeQL analyzer that does not complete, and an unexpected +Dependency Graph API error are all errors, which keeps an informational check +from silently degrading into a no-op. None of these checks are required +statuses, so they do not gate merges. + See `CI.md` for the contributor workflow, labels, and maintainer merge-queue workflow. ## Docs Site diff --git a/e2e/rust/e2e-podman-rootless.sh b/e2e/rust/e2e-podman-rootless.sh deleted file mode 100755 index d7fb5acd3c..0000000000 --- a/e2e/rust/e2e-podman-rootless.sh +++ /dev/null @@ -1,19 +0,0 @@ -#!/usr/bin/env bash -# SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. -# SPDX-License-Identifier: Apache-2.0 - -# Run the Podman e2e suite and verify rootless mode. -# -# Identical to e2e-podman.sh but fails fast if Podman is not running -# rootless. Use this to explicitly validate the rootless networking -# path (pasta, host-gateway, bind address). - -set -euo pipefail - -rootless="$(podman info --format '{{.Host.Security.Rootless}}' 2>/dev/null || true)" -if [ "${rootless}" != "true" ]; then - echo "ERROR: podman is not running rootless; expected true, got '${rootless:-}'" >&2 - exit 2 -fi - -exec "$(dirname "${BASH_SOURCE[0]}")/e2e-podman.sh" diff --git a/flake.nix b/flake.nix index 8e9fb728b1..68e6156e3b 100644 --- a/flake.nix +++ b/flake.nix @@ -46,6 +46,7 @@ }; testGuestPkgs = import nixpkgs-test-guest { inherit system; }; commonDevShellPackages = with pkgs; [ + actionlint cargo-deny cargo-nextest # Assemble Debian artifacts on macOS and Linux. @@ -55,6 +56,7 @@ # Coverage. lcov uv + zizmor ]; treefmtEval = treefmt-nix.lib.evalModule pkgs { projectRootFile = "flake.nix"; diff --git a/nix/test-guest/README.md b/nix/test-guest/README.md index ca62f893e6..b608cb19fe 100644 --- a/nix/test-guest/README.md +++ b/nix/test-guest/README.md @@ -63,6 +63,11 @@ The root [`flake.nix`](../../flake.nix) exposes this directory as the `test-gues | Fedora 44 | No | Yes | Yes | `.rpm` | | Rocky Linux 9 | Yes | Yes | Yes | `.rpm` | +The `snapd` configuration is available for Ubuntu and prepares snapd for +local Snap lifecycle experiments. It does not install Docker, because the Snap +gateway reproduction uses the Docker **Snap** and its `docker:docker-daemon` +interface rather than the host-package Docker configuration. + The Ubuntu 24.04 Podman configuration is available for runtime and packaging checks, but its Podman 4 release does not provide the `pasta` rootless network helper required by OpenShell sandbox callbacks. OpenShell Podman E2E runs use @@ -121,9 +126,11 @@ nix run .#test-guest -- \ Configurations are Ansible playbooks stored under `nix/test-guest/configuration/`. Ansible runs on the host using the VM's ephemeral SSH key and loopback port. The guest does not install Ansible. -Configurations run in the order provided on the command line. OpenShell packages and copied binaries are installed after all configurations succeed. +Configurations run in the order provided on the command line. OpenShell packages and copied files are installed after all configurations succeed. -`--install` packages and `--copy` executables are applied by a dedicated per-run Ansible playbook. They are not stored in prepared VM cache entries. +`--install` packages and `--copy` files are applied by a dedicated per-run +Ansible playbook. `--copy` preserves each source file's ordinary permission +bits. They are not stored in prepared VM cache entries. ## Prepared VM cache @@ -212,9 +219,10 @@ For an x86_64 Linux guest, supply x86_64 binaries and use `package:deb:amd64`. T `--install` is repeatable. Debian packages are accepted by Ubuntu; RPM packages are accepted by CentOS, Fedora, and Rocky Linux. This prototype can install an existing RPM but does not build one. -## Copy binaries directly +## Copy files directly -Use `--copy SOURCE:DEST` to install an executable without creating a package: +Use `--copy SOURCE:DEST` to copy a regular file without creating a package. The +guest file preserves the source's ordinary permission bits: ```shell nix run .#test-guest -- \ @@ -223,6 +231,29 @@ nix run .#test-guest -- \ -- openshell --version ``` +## Reproduce Snap gateway startup + +The gateway Snap must be native to the guest architecture. Copy an existing +Snap artifact and the reproduction script into a prepared Ubuntu guest, then +run the script as root. It follows the Release Canary ordering exactly: install +the Snap, connect Docker/log/system interfaces, and immediately query the +gateway. On each failure it prints snapd and gateway journals. + +```shell +nix run .#test-guest -- \ + --distro ubuntu \ + --with snapd \ + --keep \ + --copy ./openshell_*.snap:/tmp/openshell.snap \ + --copy ./nix/test-guest/scripts/snap-gateway-repro.sh:/usr/local/bin/snap-gateway-repro \ + -- sudo /usr/local/bin/snap-gateway-repro /tmp/openshell.snap 10 30 +``` + +`--keep` retains the overlay and serial log when diagnosing a failure. The +runner prints their location after shutdown. The final `30` accepts automatic +recovery for up to 30 seconds; omit it to require the canary's immediate check. + + The destination must be an absolute guest path. Copied files are installed with mode `0755`. ## Runner options @@ -231,7 +262,8 @@ The destination must be an absolute guest path. Copied files are installed with --distro NAME Base distro: ubuntu, centos, fedora, or rocky --with NAME Apply docker, podman, or selinux; repeatable --install PATH Install a .deb or .rpm package; repeatable ---copy SRC:DEST Copy an executable into the guest; repeatable +--copy SRC:DEST Copy a regular file into the guest, preserving its host mode; + repeatable --ssh-port PORT Use a specific loopback SSH forwarding port --forward-port HOST_PORT:GUEST_PORT Forward a loopback host port to a guest port; repeatable diff --git a/nix/test-guest/cache-seal.sh b/nix/test-guest/cache-seal.sh old mode 100644 new mode 100755 diff --git a/nix/test-guest/configuration/snapd.yml b/nix/test-guest/configuration/snapd.yml new file mode 100644 index 0000000000..c89b81a2aa --- /dev/null +++ b/nix/test-guest/configuration/snapd.yml @@ -0,0 +1,38 @@ +--- +# SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 + +- name: Configure snapd + hosts: test_vm + become: true + gather_facts: true + + tasks: + - name: Validate snapd support + ansible.builtin.assert: + that: + - ansible_facts.distribution == "Ubuntu" + fail_msg: >- + snapd is currently configured only for Ubuntu test guests, + not {{ ansible_facts.distribution }}. + + - name: Refresh Ubuntu package metadata + ansible.builtin.apt: + update_cache: true + + - name: Install snapd + ansible.builtin.apt: + name: snapd + state: present + install_recommends: false + + - name: Start snapd socket activation + ansible.builtin.systemd_service: + name: snapd.socket + enabled: true + state: started + + - name: Wait for snapd seed + ansible.builtin.command: + cmd: snap wait system seed.loaded + changed_when: false diff --git a/nix/test-guest/default.nix b/nix/test-guest/default.nix index 72937cfec2..753f266ab4 100644 --- a/nix/test-guest/default.nix +++ b/nix/test-guest/default.nix @@ -28,6 +28,7 @@ let docker = ./configuration/docker.yml; podman = ./configuration/podman.yml; selinux = ./configuration/selinux.yml; + snapd = ./configuration/snapd.yml; }; mkDistroProfile = diff --git a/nix/test-guest/run.sh b/nix/test-guest/run.sh index c999679492..6be58eb235 100644 --- a/nix/test-guest/run.sh +++ b/nix/test-guest/run.sh @@ -13,9 +13,10 @@ Usage: Options: --distro NAME Base distro: ubuntu, centos, fedora, or rocky - --with NAME Apply a configuration; repeatable (docker, podman, selinux) + --with NAME Apply a configuration; repeatable (docker, podman, selinux, snapd) --install PATH Install a .deb or .rpm package; repeatable - --copy SRC:DEST Copy an executable to an absolute guest path; repeatable + --copy SRC:DEST Copy a regular file to an absolute guest path, preserving + its host mode; repeatable --ssh-port PORT Use a specific loopback SSH forwarding port --forward-port HOST_PORT:GUEST_PORT Forward a loopback host port to a guest port; repeatable @@ -43,6 +44,26 @@ require_value() { fi } +preserved_file_mode() { + local source_path=$1 + local source_mode + + if [ "$(uname -s)" = Darwin ]; then + if ! source_mode=$(stat -f '%Lp' "${source_path}"); then + echo "could not determine mode for --copy source: ${source_path}" >&2 + return 1 + fi + elif ! source_mode=$(stat -c '%a' "${source_path}"); then + echo "could not determine mode for --copy source: ${source_path}" >&2 + return 1 + fi + if [[ ! ${source_mode} =~ ^[0-7]{3,4}$ ]]; then + echo "could not determine mode for --copy source: ${source_path}" >&2 + return 1 + fi + printf '%03o\n' "$((8#${source_mode} & 8#777))" +} + distro= requested_ssh_port= keep=0 @@ -618,13 +639,14 @@ if [ "${#packages[@]}" -gt 0 ] || [ "${#copies[@]}" -gt 0 ]; then for copy_spec in "${copies[@]}"; do source_path=${copy_spec%%:*} destination=${copy_spec#*:} + mode=$(preserved_file_mode "${source_path}") || exit 2 remote_path=${artifact_staging_dir}/copy-${artifact_index} echo "==> Copying artifact: ${destination}" scp -q "${scp_args[@]}" \ "${source_path}" "openshell@127.0.0.1:${remote_path}" printf -v install_command \ - 'sudo install -D -m 0755 -- %q %q' \ - "${remote_path}" "${destination}" + 'sudo install -D -m %q -- %q %q' \ + "${mode}" "${remote_path}" "${destination}" ssh "${ssh_args[@]}" openshell@127.0.0.1 "${install_command}" artifact_index=$((artifact_index + 1)) done diff --git a/nix/test-guest/scripts/snap-gateway-repro.sh b/nix/test-guest/scripts/snap-gateway-repro.sh new file mode 100755 index 0000000000..f3090aece2 --- /dev/null +++ b/nix/test-guest/scripts/snap-gateway-repro.sh @@ -0,0 +1,115 @@ +#!/usr/bin/env bash +# SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 + +# Reproduce the Release Canary Snap lifecycle: install the OpenShell Snap, +# connect its interfaces after the daemon is started, then immediately use the +# local gateway. Run this as root inside an Ubuntu guest prepared with --with snapd. + +set -uo pipefail + +usage() { + cat <<'EOF' +Usage: snap-gateway-repro.sh SNAP_FILE [ATTEMPTS] [READY_TIMEOUT_SECONDS] + +Install SNAP_FILE repeatedly using the Release Canary interface ordering. +ATTEMPTS defaults to 1. READY_TIMEOUT_SECONDS defaults to 0, preserving the +canary's immediate readiness check. Set it to a positive value to wait for +automatic gateway recovery after the immediate check fails. Every failed +attempt prints service, connection, snap-change, journal, gateway-log, and +listener diagnostics. +EOF +} + +if [ "$#" -lt 1 ] || [ "$#" -gt 3 ]; then + usage >&2 + exit 2 +fi + +snap_file=$1 +attempts=${2:-1} +ready_timeout=${3:-0} +if [ ! -f "${snap_file}" ]; then + echo "Snap file does not exist: ${snap_file}" >&2 + exit 2 +fi +if [[ ! ${attempts} =~ ^[1-9][0-9]*$ ]]; then + echo "ATTEMPTS must be a positive integer: ${attempts}" >&2 + exit 2 +fi +if [[ ! ${ready_timeout} =~ ^[0-9]+$ ]]; then + echo "READY_TIMEOUT_SECONDS must be a non-negative integer: ${ready_timeout}" >&2 + exit 2 +fi + +diagnostics() { + local attempt=$1 + echo "========== Snap diagnostics (attempt ${attempt}) ==========" >&2 + snap services openshell >&2 || true + snap connections openshell >&2 || true + snap changes >&2 || true + systemctl status snap.openshell.gateway.service --no-pager >&2 || true + journalctl -b -u snap.openshell.gateway.service --no-pager -n 300 >&2 || true + journalctl -b -u snapd.service --no-pager -n 300 >&2 || true + snap logs openshell.gateway -n=300 >&2 || true + ss -ltnp '( sport = :17670 )' >&2 || true +} + +gateway_is_ready() { + runuser -u openshell -- /snap/bin/openshell status >/dev/null 2>&1 +} + +wait_for_gateway() { + local deadline=$((SECONDS + ready_timeout)) + while [ "${SECONDS}" -lt "${deadline}" ]; do + if gateway_is_ready; then + return 0 + fi + sleep 1 + done + gateway_is_ready +} + +if ! snap list docker >/dev/null 2>&1; then + echo "==> Installing Docker Snap" + snap install docker +fi + +failures=0 +for attempt in $(seq 1 "${attempts}"); do + echo "==> Snap gateway reproduction attempt ${attempt}/${attempts}" + snap remove --purge openshell >/dev/null 2>&1 || true + rm -rf /home/openshell/snap/openshell + + if ! snap install "${snap_file}" --dangerous || + ! snap connect openshell:docker docker:docker-daemon || + ! snap connect openshell:log-observe || + ! snap connect openshell:system-observe; then + echo "OpenShell installation or interface connection failed" >&2 + diagnostics "${attempt}" + failures=$((failures + 1)) + continue + fi + + # This deliberately does not wait for the listener. It mirrors the canary + # and exposes a daemon that fails or races after late interface connections. + if ! runuser -u openshell -- /snap/bin/openshell gateway add \ + http://127.0.0.1:17670 --local --name snap-docker || + ! runuser -u openshell -- /snap/bin/openshell gateway select snap-docker || + ! gateway_is_ready; then + if [ "${ready_timeout}" -gt 0 ] && wait_for_gateway; then + echo "Gateway recovered automatically within ${ready_timeout}s" + continue + fi + echo "Gateway was not usable immediately after interface connection" >&2 + diagnostics "${attempt}" + failures=$((failures + 1)) + fi +done + +if [ "${failures}" -gt 0 ]; then + echo "${failures}/${attempts} attempt(s) failed" >&2 + exit 1 +fi + +echo "All ${attempts} attempt(s) passed" diff --git a/sdk/go/go.mod b/sdk/go/go.mod index 900c9520c6..7855538943 100644 --- a/sdk/go/go.mod +++ b/sdk/go/go.mod @@ -7,16 +7,16 @@ require ( github.com/stretchr/testify v1.11.1 golang.org/x/oauth2 v0.36.0 golang.org/x/sync v0.22.0 - google.golang.org/grpc v1.81.1 + google.golang.org/grpc v1.82.1 google.golang.org/protobuf v1.36.11 ) require ( github.com/davecgh/go-spew v1.1.1 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect - golang.org/x/net v0.51.0 // indirect - golang.org/x/sys v0.42.0 // indirect - golang.org/x/text v0.34.0 // indirect - google.golang.org/genproto/googleapis/rpc v0.0.0-20260226221140-a57be14db171 // indirect + golang.org/x/net v0.53.0 // indirect + golang.org/x/sys v0.43.0 // indirect + golang.org/x/text v0.36.0 // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20260414002931-afd174a4e478 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect ) diff --git a/sdk/go/go.sum b/sdk/go/go.sum index 44508540a6..54d6b83e75 100644 --- a/sdk/go/go.sum +++ b/sdk/go/go.sum @@ -30,22 +30,22 @@ go.opentelemetry.io/otel/sdk/metric v1.43.0 h1:S88dyqXjJkuBNLeMcVPRFXpRw2fuwdvfC go.opentelemetry.io/otel/sdk/metric v1.43.0/go.mod h1:C/RJtwSEJ5hzTiUz5pXF1kILHStzb9zFlIEe85bhj6A= go.opentelemetry.io/otel/trace v1.43.0 h1:BkNrHpup+4k4w+ZZ86CZoHHEkohws8AY+WTX09nk+3A= go.opentelemetry.io/otel/trace v1.43.0/go.mod h1:/QJhyVBUUswCphDVxq+8mld+AvhXZLhe+8WVFxiFff0= -golang.org/x/net v0.51.0 h1:94R/GTO7mt3/4wIKpcR5gkGmRLOuE/2hNGeWq/GBIFo= -golang.org/x/net v0.51.0/go.mod h1:aamm+2QF5ogm02fjy5Bb7CQ0WMt1/WVM7FtyaTLlA9Y= +golang.org/x/net v0.53.0 h1:d+qAbo5L0orcWAr0a9JweQpjXF19LMXJE8Ey7hwOdUA= +golang.org/x/net v0.53.0/go.mod h1:JvMuJH7rrdiCfbeHoo3fCQU24Lf5JJwT9W3sJFulfgs= golang.org/x/oauth2 v0.36.0 h1:peZ/1z27fi9hUOFCAZaHyrpWG5lwe0RJEEEeH0ThlIs= golang.org/x/oauth2 v0.36.0/go.mod h1:YDBUJMTkDnJS+A4BP4eZBjCqtokkg1hODuPjwiGPO7Q= golang.org/x/sync v0.22.0 h1:SZjpbeLmrCk4xhRSZFNZW5gFUeCeFgjekvI/+gfScek= golang.org/x/sync v0.22.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0= -golang.org/x/sys v0.42.0 h1:omrd2nAlyT5ESRdCLYdm3+fMfNFE/+Rf4bDIQImRJeo= -golang.org/x/sys v0.42.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw= -golang.org/x/text v0.34.0 h1:oL/Qq0Kdaqxa1KbNeMKwQq0reLCCaFtqu2eNuSeNHbk= -golang.org/x/text v0.34.0/go.mod h1:homfLqTYRFyVYemLBFl5GgL/DWEiH5wcsQ5gSh1yziA= +golang.org/x/sys v0.43.0 h1:Rlag2XtaFTxp19wS8MXlJwTvoh8ArU6ezoyFsMyCTNI= +golang.org/x/sys v0.43.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw= +golang.org/x/text v0.36.0 h1:JfKh3XmcRPqZPKevfXVpI1wXPTqbkE5f7JA92a55Yxg= +golang.org/x/text v0.36.0/go.mod h1:NIdBknypM8iqVmPiuco0Dh6P5Jcdk8lJL0CUebqK164= gonum.org/v1/gonum v0.17.0 h1:VbpOemQlsSMrYmn7T2OUvQ4dqxQXU+ouZFQsZOx50z4= gonum.org/v1/gonum v0.17.0/go.mod h1:El3tOrEuMpv2UdMrbNlKEh9vd86bmQ6vqIcDwxEOc1E= -google.golang.org/genproto/googleapis/rpc v0.0.0-20260226221140-a57be14db171 h1:ggcbiqK8WWh6l1dnltU4BgWGIGo+EVYxCaAPih/zQXQ= -google.golang.org/genproto/googleapis/rpc v0.0.0-20260226221140-a57be14db171/go.mod h1:4Hqkh8ycfw05ld/3BWL7rJOSfebL2Q+DVDeRgYgxUU8= -google.golang.org/grpc v1.81.1 h1:VnnIIZ88UzOOKLukQi+ImGz8O1Wdp8nAGGnvOfEIWQQ= -google.golang.org/grpc v1.81.1/go.mod h1:xGH9GfzOyMTGIOXBJmXt+BX/V0kcdQbdcuwQ/zNw42I= +google.golang.org/genproto/googleapis/rpc v0.0.0-20260414002931-afd174a4e478 h1:RmoJA1ujG+/lRGNfUnOMfhCy5EipVMyvUE+KNbPbTlw= +google.golang.org/genproto/googleapis/rpc v0.0.0-20260414002931-afd174a4e478/go.mod h1:4Hqkh8ycfw05ld/3BWL7rJOSfebL2Q+DVDeRgYgxUU8= +google.golang.org/grpc v1.82.1 h1:NnAxzGRA0677vCa4BUkOAnO5+FfQqVl9iUXeD0IqcGE= +google.golang.org/grpc v1.82.1/go.mod h1:yzTZ1TB1Z3SG+LIYaI+WiE8D5+PZ3ArnrSp8zF3+/ZA= google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE= google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= diff --git a/tasks/test.toml b/tasks/test.toml index dfb632dda0..29e06b826a 100644 --- a/tasks/test.toml +++ b/tasks/test.toml @@ -136,10 +136,6 @@ run = [ "CONTAINER_RUNTIME=docker e2e/with-keycloak.sh env OPENSHELL_E2E_OIDC_GATEWAY=1 e2e/with-docker-gateway.sh uv run pytest -m 'not gpu' e2e/python/oidc", ] -["e2e:podman:rootless"] -description = "Run Rust CLI e2e tests against a rootless Podman-backed gateway" -run = "e2e/rust/e2e-podman-rootless.sh" - ["e2e:podman:gpu"] description = "Run GPU e2e against a standalone gateway with the Podman compute driver" env = { OPENSHELL_E2E_PODMAN_GPU = "1", OPENSHELL_E2E_PODMAN_TEST = "gpu", OPENSHELL_E2E_PODMAN_FEATURES = "e2e-podman-gpu" }