fix: pass codex output flag to exec #374
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| name: Enterprise CI | |
| on: | |
| push: | |
| branches: ["**"] | |
| tags: | |
| - "v*" | |
| pull_request: | |
| branches: ["main"] | |
| workflow_dispatch: | |
| concurrency: | |
| group: ci-${{ github.ref }} | |
| cancel-in-progress: true | |
| permissions: | |
| contents: read | |
| pull-requests: read | |
| env: | |
| GOFLAGS: -mod=readonly | |
| GO_VERSION: "1.25.10" | |
| jobs: | |
| quick-checks: | |
| name: Quick Checks | |
| if: github.event_name == 'push' && github.ref != 'refs/heads/main' && !startsWith(github.ref, 'refs/tags/v') | |
| runs-on: ubuntu-latest | |
| timeout-minutes: 10 | |
| steps: | |
| - name: Checkout | |
| uses: actions/checkout@v6.0.2 | |
| - name: Setup Go | |
| uses: actions/setup-go@v6.3.0 | |
| with: | |
| go-version: ${{ env.GO_VERSION }} | |
| cache: true | |
| - name: Test | |
| run: go test ./... | |
| - name: Vet | |
| run: go vet ./... | |
| - name: Build | |
| run: go build ./... | |
| dependency-review: | |
| name: Dependency Review | |
| if: github.event_name == 'pull_request' | |
| runs-on: ubuntu-latest | |
| permissions: | |
| contents: read | |
| pull-requests: write | |
| steps: | |
| - name: Checkout | |
| uses: actions/checkout@v6.0.2 | |
| - name: Dependency Review | |
| uses: actions/dependency-review-action@v4.9.0 | |
| workflow-lint: | |
| name: Workflow Lint | |
| if: github.event_name != 'push' || github.ref == 'refs/heads/main' || startsWith(github.ref, 'refs/tags/v') | |
| runs-on: ubuntu-latest | |
| timeout-minutes: 10 | |
| steps: | |
| - name: Checkout | |
| uses: actions/checkout@v6.0.2 | |
| - name: Setup Go | |
| uses: actions/setup-go@v6.3.0 | |
| with: | |
| go-version: ${{ env.GO_VERSION }} | |
| cache: true | |
| - name: Install actionlint | |
| run: go install github.com/rhysd/actionlint/cmd/actionlint@v1.7.7 | |
| - name: Lint workflows | |
| shell: bash | |
| run: | | |
| set -euo pipefail | |
| "$(go env GOPATH)/bin/actionlint" | |
| lint-and-static: | |
| name: Lint And Static Analysis | |
| if: github.event_name != 'push' || github.ref == 'refs/heads/main' || startsWith(github.ref, 'refs/tags/v') | |
| runs-on: ubuntu-latest | |
| timeout-minutes: 15 | |
| steps: | |
| - name: Checkout | |
| uses: actions/checkout@v6.0.2 | |
| - name: Setup Go | |
| uses: actions/setup-go@v6.3.0 | |
| with: | |
| go-version: ${{ env.GO_VERSION }} | |
| cache: true | |
| - name: Verify formatting | |
| shell: bash | |
| run: | | |
| set -euo pipefail | |
| files="$(gofmt -l .)" | |
| if [[ -n "${files}" ]]; then | |
| echo "Unformatted files detected:" | |
| echo "${files}" | |
| exit 1 | |
| fi | |
| - name: Vet | |
| run: go vet ./... | |
| - name: Build | |
| run: go build ./... | |
| test: | |
| name: Test | |
| if: github.event_name != 'push' || github.ref == 'refs/heads/main' || startsWith(github.ref, 'refs/tags/v') | |
| runs-on: ubuntu-latest | |
| timeout-minutes: 20 | |
| steps: | |
| - name: Checkout | |
| uses: actions/checkout@v6.0.2 | |
| - name: Setup Go | |
| uses: actions/setup-go@v6.3.0 | |
| with: | |
| go-version: ${{ env.GO_VERSION }} | |
| cache: true | |
| - name: Test | |
| run: go test ./... | |
| mcp-reference-contract: | |
| name: MCP Reference Contract | |
| if: github.event_name != 'push' || github.ref == 'refs/heads/main' || startsWith(github.ref, 'refs/tags/v') | |
| runs-on: ubuntu-latest | |
| timeout-minutes: 20 | |
| steps: | |
| - name: Checkout | |
| uses: actions/checkout@v6.0.2 | |
| - name: Setup Go | |
| uses: actions/setup-go@v6.3.0 | |
| with: | |
| go-version: ${{ env.GO_VERSION }} | |
| cache: true | |
| - name: Run MCP reference contract suite | |
| shell: bash | |
| env: | |
| NOMOS_MCP_CONTRACT_TESTS: "1" | |
| run: | | |
| set -euo pipefail | |
| mkdir -p artifacts | |
| go test ./internal/mcp -run "TestReferenceMCPContract|TestReferenceContractManifest" -count=1 -v -json | tee artifacts/mcp-reference-contract.json | |
| - name: Upload MCP contract artifact | |
| if: always() | |
| uses: actions/upload-artifact@v4.6.2 | |
| with: | |
| name: mcp-reference-contract | |
| path: artifacts/mcp-reference-contract.json | |
| if-no-files-found: ignore | |
| cli-policy-doctor-smoke: | |
| name: CLI + Policy + Doctor Smoke | |
| if: github.event_name != 'push' || github.ref == 'refs/heads/main' || startsWith(github.ref, 'refs/tags/v') | |
| runs-on: ubuntu-latest | |
| timeout-minutes: 20 | |
| steps: | |
| - name: Checkout | |
| uses: actions/checkout@v6.0.2 | |
| - name: Setup Go | |
| uses: actions/setup-go@v6.3.0 | |
| with: | |
| go-version: ${{ env.GO_VERSION }} | |
| cache: true | |
| - name: Build and run CLI/policy/doctor smoke scenarios | |
| shell: bash | |
| env: | |
| NOMOS_MCP_ENABLED: "true" | |
| run: | | |
| set -euo pipefail | |
| BIN_DIR="${RUNNER_TEMP}/bin" | |
| TEST_DIR="$(mktemp -d)" | |
| BIN_PATH="${BIN_DIR}/nomos" | |
| mkdir -p "${BIN_DIR}" | |
| cleanup() { | |
| rm -rf "${TEST_DIR}" | |
| } | |
| trap cleanup EXIT | |
| go build -o "${BIN_PATH}" ./cmd/nomos | |
| version_output="$("${BIN_PATH}" version)" | |
| echo "${version_output}" | |
| [[ "${version_output}" == *"version="* ]] | |
| [[ "${version_output}" == *"go="* ]] | |
| set +e | |
| no_args_output="$("${BIN_PATH}" 2>&1)" | |
| no_args_status=$? | |
| set -e | |
| echo "${no_args_output}" | |
| [[ ${no_args_status} -ne 0 ]] | |
| [[ "${no_args_output}" == *"nomos commands:"* ]] | |
| cat > "${TEST_DIR}/action-allow-readme.json" <<'EOF' | |
| { | |
| "schema_version": "v1", | |
| "action_id": "ci_policy_allow_1", | |
| "action_type": "fs.read", | |
| "resource": "file://workspace/README.md", | |
| "params": {}, | |
| "principal": "system", | |
| "agent": "nomos", | |
| "environment": "dev", | |
| "context": { "extensions": {} }, | |
| "trace_id": "ci_policy_allow_trace_1" | |
| } | |
| EOF | |
| cat > "${TEST_DIR}/action-deny-other-file.json" <<'EOF' | |
| { | |
| "schema_version": "v1", | |
| "action_id": "ci_policy_deny_1", | |
| "action_type": "fs.read", | |
| "resource": "file://workspace/.env", | |
| "params": {}, | |
| "principal": "system", | |
| "agent": "nomos", | |
| "environment": "dev", | |
| "context": { "extensions": {} }, | |
| "trace_id": "ci_policy_deny_trace_1" | |
| } | |
| EOF | |
| cat > "${TEST_DIR}/action-invalid.json" <<'EOF' | |
| { | |
| "schema_version": "v1", | |
| "action_id": "ci_invalid_1", | |
| "action_type": "fs.read", | |
| "resource": "file://workspace/README.md", | |
| "params": [], | |
| "principal": "system", | |
| "agent": "nomos", | |
| "environment": "dev", | |
| "context": { "extensions": {} }, | |
| "trace_id": "ci_invalid_trace_1" | |
| } | |
| EOF | |
| cat > "${TEST_DIR}/action-traversal.json" <<'EOF' | |
| { | |
| "schema_version": "v1", | |
| "action_id": "ci_traversal_1", | |
| "action_type": "fs.read", | |
| "resource": "file://workspace/../.env", | |
| "params": {}, | |
| "principal": "system", | |
| "agent": "nomos", | |
| "environment": "dev", | |
| "context": { "extensions": {} }, | |
| "trace_id": "ci_traversal_trace_1" | |
| } | |
| EOF | |
| "${BIN_PATH}" policy test --action "${TEST_DIR}/action-allow-readme.json" --bundle ./examples/policies/safe.json > "${TEST_DIR}/policy-allow.json" | |
| cat "${TEST_DIR}/policy-allow.json" | |
| "${BIN_PATH}" policy test --action "${TEST_DIR}/action-deny-other-file.json" --bundle ./examples/policies/safe.json > "${TEST_DIR}/policy-deny.json" | |
| cat "${TEST_DIR}/policy-deny.json" | |
| "${BIN_PATH}" policy test --action "${TEST_DIR}/action-allow-readme.json" --bundle ./examples/policies/safe.yaml > "${TEST_DIR}/policy-yaml.json" | |
| "${BIN_PATH}" policy test --action "${TEST_DIR}/action-allow-readme.json" --bundle ./examples/policies/safe.json > "${TEST_DIR}/policy-json.json" | |
| "${BIN_PATH}" policy explain --action "${TEST_DIR}/action-deny-other-file.json" --bundle ./examples/policies/safe.json > "${TEST_DIR}/policy-explain.json" | |
| python3 - <<'PY' "${TEST_DIR}" | |
| import json | |
| import pathlib | |
| import sys | |
| root = pathlib.Path(sys.argv[1]) | |
| allow_payload = json.loads((root / "policy-allow.json").read_text()) | |
| assert allow_payload["decision"] == "ALLOW", allow_payload | |
| assert allow_payload["policy_bundle_hash"], allow_payload | |
| deny_payload = json.loads((root / "policy-deny.json").read_text()) | |
| assert deny_payload["decision"] == "DENY", deny_payload | |
| assert deny_payload["reason_code"] in {"deny_by_default", "deny_by_rule"}, deny_payload | |
| yaml_payload = json.loads((root / "policy-yaml.json").read_text()) | |
| json_payload = json.loads((root / "policy-json.json").read_text()) | |
| assert yaml_payload["decision"] == "ALLOW", yaml_payload | |
| assert json_payload["decision"] == "ALLOW", json_payload | |
| assert yaml_payload["policy_bundle_hash"] == json_payload["policy_bundle_hash"], (yaml_payload, json_payload) | |
| explain_payload = json.loads((root / "policy-explain.json").read_text()) | |
| assert explain_payload["decision"] == "DENY", explain_payload | |
| assert "assurance_level" in explain_payload, explain_payload | |
| assert "why_denied" in explain_payload, explain_payload | |
| assert "remediation_hint" in explain_payload["why_denied"], explain_payload | |
| PY | |
| set +e | |
| missing_bundle_output="$("${BIN_PATH}" policy test --action "${TEST_DIR}/action-allow-readme.json" --bundle "${TEST_DIR}/missing.json" 2>&1)" | |
| missing_bundle_status=$? | |
| invalid_action_output="$("${BIN_PATH}" policy test --action "${TEST_DIR}/action-invalid.json" --bundle ./examples/policies/safe.yaml 2>&1)" | |
| invalid_action_status=$? | |
| traversal_output="$("${BIN_PATH}" policy test --action "${TEST_DIR}/action-traversal.json" --bundle ./examples/policies/safe.yaml 2>&1)" | |
| traversal_status=$? | |
| set -e | |
| echo "${missing_bundle_output}" | |
| echo "${invalid_action_output}" | |
| echo "${traversal_output}" | |
| [[ ${missing_bundle_status} -ne 0 ]] | |
| [[ "${missing_bundle_output}" == *"VALIDATION_ERROR:"* ]] | |
| [[ ${invalid_action_status} -ne 0 ]] | |
| [[ "${invalid_action_output}" == *"VALIDATION_ERROR:"* ]] | |
| [[ ${traversal_status} -ne 0 ]] | |
| [[ "${traversal_output}" == *"NORMALIZATION_ERROR:"* ]] | |
| "${BIN_PATH}" doctor -c ./testdata/ci/config.smoke.json > "${TEST_DIR}/doctor.txt" | |
| cat "${TEST_DIR}/doctor.txt" | |
| grep -q "Result: READY" "${TEST_DIR}/doctor.txt" | |
| "${BIN_PATH}" doctor -c ./testdata/ci/config.smoke.json --format json > "${TEST_DIR}/doctor.json" | |
| cat "${TEST_DIR}/doctor.json" | |
| python3 - <<'PY' "${TEST_DIR}/doctor.json" | |
| import json | |
| import pathlib | |
| import sys | |
| payload = json.loads(pathlib.Path(sys.argv[1]).read_text()) | |
| assert payload["overall_status"] == "READY", payload | |
| assert payload["checks"], payload | |
| assert payload["policy_bundle_hash"], payload | |
| PY | |
| python3 - <<'PY' ./testdata/ci/config.smoke.json "${TEST_DIR}/config-doctor-missing-bundle.json" | |
| import json | |
| import pathlib | |
| import sys | |
| source = pathlib.Path(sys.argv[1]) | |
| target = pathlib.Path(sys.argv[2]) | |
| payload = json.loads(source.read_text()) | |
| payload["policy"]["policy_bundle_path"] = "./examples/policies/missing.yaml" | |
| target.write_text(json.dumps(payload, indent=2) + "\n") | |
| PY | |
| set +e | |
| doctor_not_ready_output="$("${BIN_PATH}" doctor -c "${TEST_DIR}/config-doctor-missing-bundle.json" 2>&1)" | |
| doctor_not_ready_status=$? | |
| doctor_bad_format_output="$("${BIN_PATH}" doctor -c ./testdata/ci/config.smoke.json --format yaml 2>&1)" | |
| doctor_bad_format_status=$? | |
| set -e | |
| echo "${doctor_not_ready_output}" | |
| echo "${doctor_bad_format_output}" | |
| [[ ${doctor_not_ready_status} -eq 1 ]] | |
| [[ "${doctor_not_ready_output}" == *"NOT_READY"* || "${doctor_not_ready_output}" == *"doctor completed: status=NOT_READY"* ]] | |
| [[ ${doctor_bad_format_status} -eq 2 ]] | |
| [[ "${doctor_bad_format_output}" == *"invalid --format value"* && "${doctor_bad_format_output}" == *"expected text or json"* ]] | |
| - name: MCP capability and not-found smoke | |
| run: go test ./internal/mcp -run "TestCapabilitiesSurfaceAssuranceLevelAndNotice|TestHandleRequestMapsMissingFilesToNotFound" | |
| normalization-corpus-matrix: | |
| name: Normalization Corpus Matrix (${{ matrix.os }}) | |
| if: github.event_name != 'push' || github.ref == 'refs/heads/main' || startsWith(github.ref, 'refs/tags/v') | |
| strategy: | |
| fail-fast: false | |
| matrix: | |
| os: [ubuntu-latest, macos-latest, windows-latest] | |
| runs-on: ${{ matrix.os }} | |
| timeout-minutes: 20 | |
| steps: | |
| - name: Checkout | |
| uses: actions/checkout@v6.0.2 | |
| - name: Setup Go | |
| uses: actions/setup-go@v6.3.0 | |
| with: | |
| go-version: ${{ env.GO_VERSION }} | |
| cache: true | |
| - name: Normalization corpus digest | |
| run: go test ./internal/normalize -run "TestNormalizationGoldenCorpus|TestNormalizationBypassCorpus|TestNormalizationCorpusDigestStable" | |
| bypass-suite: | |
| name: Bypass Suite | |
| if: github.event_name != 'push' || github.ref == 'refs/heads/main' || startsWith(github.ref, 'refs/tags/v') | |
| runs-on: ubuntu-latest | |
| timeout-minutes: 20 | |
| steps: | |
| - name: Checkout | |
| uses: actions/checkout@v6.0.2 | |
| - name: Setup Go | |
| uses: actions/setup-go@v6.3.0 | |
| with: | |
| go-version: ${{ env.GO_VERSION }} | |
| cache: true | |
| - name: Run bypass suite | |
| run: go test ./internal/bypasssuite | |
| race-and-smoke: | |
| name: Race + Doctor Smoke | |
| if: github.event_name != 'push' || github.ref == 'refs/heads/main' || startsWith(github.ref, 'refs/tags/v') | |
| runs-on: ubuntu-latest | |
| timeout-minutes: 20 | |
| steps: | |
| - name: Checkout | |
| uses: actions/checkout@v6.0.2 | |
| - name: Setup Go | |
| uses: actions/setup-go@v6.3.0 | |
| with: | |
| go-version: ${{ env.GO_VERSION }} | |
| cache: true | |
| - name: Race tests | |
| run: go test -race ./... | |
| - name: Doctor smoke (ready) | |
| shell: bash | |
| env: | |
| NOMOS_MCP_ENABLED: "true" | |
| run: | | |
| set -euo pipefail | |
| go run ./cmd/nomos doctor -c ./testdata/ci/config.smoke.json --format json > doctor.json | |
| cat doctor.json | |
| grep -q '"overall_status":"READY"' doctor.json | |
| vulnerability-scan: | |
| name: Vulnerability Scan | |
| if: github.event_name != 'push' || github.ref == 'refs/heads/main' || startsWith(github.ref, 'refs/tags/v') | |
| runs-on: ubuntu-latest | |
| timeout-minutes: 15 | |
| steps: | |
| - name: Checkout | |
| uses: actions/checkout@v6.0.2 | |
| - name: Setup Go | |
| uses: actions/setup-go@v6.3.0 | |
| with: | |
| go-version: ${{ env.GO_VERSION }} | |
| cache: true | |
| - name: Install govulncheck | |
| run: go install golang.org/x/vuln/cmd/govulncheck@v1.1.4 | |
| - name: Run govulncheck | |
| run: govulncheck ./... | |
| release-dry-run: | |
| name: Release Dry Run | |
| if: github.event_name == 'pull_request' | |
| runs-on: ubuntu-latest | |
| timeout-minutes: 20 | |
| steps: | |
| - name: Checkout | |
| uses: actions/checkout@v6.0.2 | |
| - name: Setup Go | |
| uses: actions/setup-go@v6.3.0 | |
| with: | |
| go-version: ${{ env.GO_VERSION }} | |
| cache: true | |
| - name: Dry-run release build | |
| shell: bash | |
| run: | | |
| set -euo pipefail | |
| ROOT_DIR="${PWD}" | |
| mkdir -p dist | |
| VERSION="v0.0.0-dry-run" | |
| COMMIT="${GITHUB_SHA::12}" | |
| BUILD_DATE="1970-01-01T00:00:00Z" | |
| : > dist/nomos-checksums.txt | |
| for target in \ | |
| "linux amd64 tar.gz" \ | |
| "linux arm64 tar.gz" \ | |
| "darwin amd64 tar.gz" \ | |
| "darwin arm64 tar.gz" \ | |
| "windows amd64 zip" \ | |
| "windows arm64 zip"; do | |
| read -r GOOS_TARGET GOARCH_TARGET PACKAGE_FORMAT <<<"${target}" | |
| STAGE_DIR="$(mktemp -d)" | |
| BIN_NAME="nomos" | |
| if [[ "${GOOS_TARGET}" == "windows" ]]; then | |
| BIN_NAME="nomos.exe" | |
| fi | |
| GOOS="${GOOS_TARGET}" GOARCH="${GOARCH_TARGET}" \ | |
| go build -ldflags "-X github.com/safe-agentic-world/nomos/internal/version.Version=${VERSION} -X github.com/safe-agentic-world/nomos/internal/version.Commit=${COMMIT} -X github.com/safe-agentic-world/nomos/internal/version.BuildDate=${BUILD_DATE}" \ | |
| -o "${STAGE_DIR}/${BIN_NAME}" ./cmd/nomos | |
| OUT="dist/nomos-${GOOS_TARGET}-${GOARCH_TARGET}.${PACKAGE_FORMAT}" | |
| if [[ "${PACKAGE_FORMAT}" == "zip" ]]; then | |
| (cd "${STAGE_DIR}" && zip -q "${ROOT_DIR}/${OUT}" "${BIN_NAME}") | |
| else | |
| tar -C "${STAGE_DIR}" -czf "${OUT}" "${BIN_NAME}" | |
| fi | |
| (cd dist && sha256sum "nomos-${GOOS_TARGET}-${GOARCH_TARGET}.${PACKAGE_FORMAT}") >> dist/nomos-checksums.txt | |
| rm -rf "${STAGE_DIR}" | |
| done |