diff --git a/.github/workflows/candidate.yml b/.github/workflows/candidate.yml index e272be3..75ca4f3 100644 --- a/.github/workflows/candidate.yml +++ b/.github/workflows/candidate.yml @@ -66,8 +66,18 @@ jobs: - uses: ruby/setup-ruby@003a5c4d8d6321bd302e38f6f0ec593f77f06600 # v1 with: ruby-version: "3.4" - bundler-cache: true + bundler-cache: ${{ github.server_url == 'https://github.com' }} working-directory: ruby-client + - name: Install candidate dependencies without the unavailable Gitea cache service + if: github.server_url != 'https://github.com' + working-directory: ruby-client + env: + BUNDLE_APP_CONFIG: ${{ runner.temp }}/opencode-ruby-fixture-bundle-config + BUNDLE_PATH: ${{ runner.temp }}/opencode-ruby-fixture-bundle + run: | + echo "BUNDLE_APP_CONFIG=$BUNDLE_APP_CONFIG" >> "$GITHUB_ENV" + echo "BUNDLE_PATH=$BUNDLE_PATH" >> "$GITHUB_ENV" + bundle install --jobs 4 --retry 3 - id: client-sha name: Verify immutable Ruby candidate checkout env: @@ -94,7 +104,7 @@ jobs: mkdir -p ../.compat-evidence bundle exec ruby ../ruby/opencode_ruby_fixture_contract.rb - name: Upload fixture evidence - if: always() + if: always() && github.server_url == 'https://github.com' uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 with: name: ruby-fixtures-${{ github.run_id }}-${{ github.run_attempt }} @@ -102,6 +112,9 @@ jobs: include-hidden-files: true if-no-files-found: warn retention-days: 30 + - name: State the Gitea evidence boundary + if: always() && github.server_url != 'https://github.com' + run: echo "Gitea executed the fixture contract, but this forge has no supported immutable artifact upload; generated JSON is transient and is not review evidence." lockstep-client-contract: needs: prepare @@ -130,8 +143,18 @@ jobs: - uses: ruby/setup-ruby@003a5c4d8d6321bd302e38f6f0ec593f77f06600 # v1 with: ruby-version: ${{ matrix.ruby }} - bundler-cache: true + bundler-cache: ${{ github.server_url == 'https://github.com' }} working-directory: opencode-rails + - name: Install candidate dependencies without the unavailable Gitea cache service + if: github.server_url != 'https://github.com' + working-directory: opencode-rails + env: + BUNDLE_APP_CONFIG: ${{ runner.temp }}/opencode-rails-${{ matrix.ruby }}-bundle-config + BUNDLE_PATH: ${{ runner.temp }}/opencode-rails-${{ matrix.ruby }}-bundle + run: | + echo "BUNDLE_APP_CONFIG=$BUNDLE_APP_CONFIG" >> "$GITHUB_ENV" + echo "BUNDLE_PATH=$BUNDLE_PATH" >> "$GITHUB_ENV" + bundle install --jobs 4 --retry 3 - name: Verify immutable candidate checkouts env: EXPECTED_RUBY_SHA: ${{ needs.prepare.outputs.ruby_ref }} @@ -168,7 +191,7 @@ jobs: mkdir -p ../.compat-evidence bundle exec ruby ../ruby/lockstep_client_contract.rb - name: Upload lockstep evidence - if: always() + if: always() && github.server_url == 'https://github.com' uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 with: name: lockstep-ruby-${{ matrix.ruby }}-${{ github.run_id }}-${{ github.run_attempt }} @@ -176,8 +199,12 @@ jobs: include-hidden-files: true if-no-files-found: warn retention-days: 30 + - name: State the Gitea evidence boundary + if: always() && github.server_url != 'https://github.com' + run: echo "Gitea executed the lockstep client contract, but this forge has no supported immutable artifact upload; generated JSON is transient and is not review evidence." exact-image-contract: + if: github.server_url == 'https://github.com' needs: prepare runs-on: ubuntu-latest strategy: @@ -230,7 +257,7 @@ jobs: mkdir -p .compat-evidence bundle exec scripts/run_image_contract.sh - name: Upload exact-image evidence - if: always() + if: always() && github.server_url == 'https://github.com' uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 with: name: image-${{ matrix.id }}-${{ github.run_id }}-${{ github.run_attempt }} @@ -238,3 +265,51 @@ jobs: include-hidden-files: true if-no-files-found: warn retention-days: 30 + + exact-image-contract-gitea: + if: github.server_url != 'https://github.com' + needs: prepare + runs-on: ubuntu-latest + name: Gitea full exact image matrix + steps: + - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 + - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 + with: + repository: ajaynomics/opencode-ruby + ref: ${{ needs.prepare.outputs.ruby_ref }} + path: ruby-client + - uses: ruby/setup-ruby@003a5c4d8d6321bd302e38f6f0ec593f77f06600 # v1 + with: + ruby-version: "3.4" + - name: Install candidate dependencies outside the checkout + working-directory: ruby-client + env: + BUNDLE_APP_CONFIG: ${{ runner.temp }}/opencode-ruby-bundle-config + BUNDLE_PATH: ${{ runner.temp }}/opencode-ruby-bundle + run: | + echo "BUNDLE_APP_CONFIG=$BUNDLE_APP_CONFIG" >> "$GITHUB_ENV" + echo "BUNDLE_PATH=$BUNDLE_PATH" >> "$GITHUB_ENV" + bundle install --jobs 4 --retry 3 + - id: client-sha + name: Verify immutable Ruby candidate checkout + env: + EXPECTED_CLIENT_SHA: ${{ needs.prepare.outputs.ruby_ref }} + run: | + actual_client_sha="$(git -C ruby-client rev-parse HEAD)" + test "$actual_client_sha" = "$EXPECTED_CLIENT_SHA" + echo "value=$actual_client_sha" >> "$GITHUB_OUTPUT" + - name: Exercise the complete manifest matrix without artifact claims + env: + OPENCODE_RUBY_PATH: ${{ github.workspace }}/ruby-client + OPENCODE_RUBY_COMMIT: ${{ steps.client-sha.outputs.value }} + OPENCODE_RAILS_COMMIT: ${{ needs.prepare.outputs.rails_ref }} + OPENCODE_COMPAT_EVIDENCE_DIR: ${{ github.workspace }}/.compat-evidence + OPENCODE_COMPAT_RUN_ID: ${{ github.run_id }} + OPENCODE_COMPAT_RUN_ATTEMPT: ${{ github.run_attempt }} + OPENCODE_COMPAT_HEAD_SHA: ${{ github.sha }} + OPENCODE_COMPAT_REPOSITORY: ${{ github.repository }} + OPENCODE_COMPAT_RUN_URL: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }} + run: scripts/run_image_matrix_contract.sh + - name: State the Gitea evidence boundary + if: always() + run: echo "Gitea executed every manifest image in one job, but this forge has no supported immutable artifact upload; generated JSON is transient and is not review evidence." diff --git a/README.md b/README.md index 7fc59b0..310cdaf 100644 --- a/README.md +++ b/README.md @@ -82,11 +82,16 @@ expected text byte-for-byte and the deterministic model stub to observe exactly one request and exactly one authoritative assistant message. It never calls an external model provider. -Candidate CI uploads machine-readable fixture, lockstep-client, and exact-image -evidence for 30 days. These artifacts are review inputs, not certification by -themselves. A person must review a passing artifact and commit the durable -certification document under `evidence/`; no workflow commits or promotes its -own result. +On GitHub, candidate CI uploads machine-readable fixture, lockstep-client, and +exact-image evidence for 30 days. These artifacts are review inputs, not +certification by themselves. Gitea runs the same repository, fixture, and +lockstep contracts and executes every exact-image manifest entry sequentially, +because its runner cannot expand a matrix from a dependency job's JSON output. +The installed Gitea artifact service does not support the pinned GitHub upload +action, so Gitea deliberately uploads nothing: its generated JSON is transient +and must not be described as review evidence. A person must review a passing +GitHub artifact and commit the durable certification document under `evidence/`; +no workflow commits or promotes its own result. `manifests/image-matrix.json` binds the active pending matrix to both candidate commits. Its `previous_certification` entries preserve the last alpha7 result; diff --git a/docs/certification.md b/docs/certification.md index c730532..6d6c571 100644 --- a/docs/certification.md +++ b/docs/certification.md @@ -126,12 +126,16 @@ still does not certify a consumer's ActiveRecord schema, persistence callbacks, container adapter, or application canary; those remain consumer-owned profile evidence. -CI writes canonical JSON artifacts for the shared fixture, lockstep client, and -each exact-image target. Artifact retention is 30 days and supplies reviewable -workflow provenance; it is not the long-term ledger. After review, copy the -relevant facts into a repository evidence document bound to the complete tuple -fingerprint. Automated workflows never update certified evidence or promote a -tuple. +GitHub CI writes canonical JSON artifacts for the shared fixture, lockstep +client, and each exact-image target. Artifact retention is 30 days and supplies +reviewable workflow provenance; it is not the long-term ledger. Gitea executes +the same contracts, with its exact-image targets run sequentially from the full +generated manifest, but its installed artifact service cannot accept the +reviewed GitHub upload action. Gitea therefore keeps generated JSON transient +and makes no artifact-evidence claim. After reviewing a GitHub artifact, copy +the relevant facts into a repository evidence document bound to the complete +tuple fingerprint. Automated workflows never update certified evidence or +promote a tuple. The command clears `candidate` after promotion. It sets the repository-wide `migration_state` to `certified` only after every consumer has certified diff --git a/scripts/run_image_matrix_contract.sh b/scripts/run_image_matrix_contract.sh new file mode 100755 index 0000000..d1671eb --- /dev/null +++ b/scripts/run_image_matrix_contract.sh @@ -0,0 +1,44 @@ +#!/usr/bin/env bash +set -euo pipefail + +repo_root="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" +evidence_dir="${OPENCODE_COMPAT_EVIDENCE_DIR:-$repo_root/.compat-evidence}" +matrix_json="$(ruby "$repo_root/scripts/matrix_json.rb")" +entry_count="$(jq -er '.include | length' <<<"$matrix_json")" + +if [[ ! "$entry_count" =~ ^[1-9][0-9]*$ ]]; then + echo "exact image matrix must contain at least one entry" >&2 + exit 2 +fi + +mkdir -p "$evidence_dir" + +for ((index = 0; index < entry_count; index++)); do + entry="$(jq -ec --argjson index "$index" '.include[$index]' <<<"$matrix_json")" + matrix_id="$(jq -er '.id' <<<"$entry")" + image="$(jq -er '.image' <<<"$entry")" + expected_version="$(jq -er '.version' <<<"$entry")" + required_profiles="$(jq -ec '.required_consumer_profiles' <<<"$entry")" + evidence_path="$evidence_dir/$matrix_id.json" + + printf 'Running exact image matrix entry %s (%s)\n' "$matrix_id" "$image" + OPENCODE_IMAGE="$image" \ + OPENCODE_EXPECTED_VERSION="$expected_version" \ + OPENCODE_MATRIX_ID="$matrix_id" \ + OPENCODE_REQUIRED_CONSUMER_PROFILES="$required_profiles" \ + OPENCODE_COMPAT_EVIDENCE_PATH="$evidence_path" \ + bundle exec "$repo_root/scripts/run_image_contract.sh" + + jq -e \ + --arg matrix_id "$matrix_id" \ + --arg image "$image" \ + --arg version "$expected_version" \ + '.status == "pass" and + .matrix_target == $matrix_id and + .image.requested == $image and + .image.reported_version == $version' \ + "$evidence_path" >/dev/null + printf 'Exact image matrix entry %s passed; JSON remains transient on this forge.\n' "$matrix_id" +done + +printf 'Full exact image matrix passed (%s entries).\n' "$entry_count" diff --git a/test/repository_test.rb b/test/repository_test.rb index 7c47537..3a8e0c3 100644 --- a/test/repository_test.rb +++ b/test/repository_test.rb @@ -275,6 +275,35 @@ class RepositoryTest < Minitest::Test refute_match(/\b(kamal|kubectl|helm|nomad|docker\s+service|gh\s+pr\s+merge)\b/i, workflow) end + def test_candidate_workflow_has_an_explicit_dual_forge_evidence_boundary + workflow = File.read(File.join(ROOT, ".github/workflows/candidate.yml")) + upload_conditions = workflow.scan( + /- name: Upload (?:fixture|lockstep|exact-image) evidence\n\s+if: ([^\n]+)\n\s+uses: actions\/upload-artifact@/ + ).flatten + + assert_equal 3, upload_conditions.length + assert_equal ["always() && github.server_url == 'https://github.com'"], upload_conditions.uniq + assert_includes workflow, "bundler-cache: ${{ github.server_url == 'https://github.com' }}" + assert_includes workflow, "exact-image-contract-gitea:" + assert_includes workflow, "if: github.server_url != 'https://github.com'" + assert_includes workflow, "run: scripts/run_image_matrix_contract.sh" + assert_operator workflow.scan("generated JSON is transient and is not review evidence").length, :>=, 3 + + gitea_job = workflow.split(/^ exact-image-contract-gitea:\n/, 2).fetch(1) + refute_includes gitea_job, "actions/upload-artifact@" + end + + def test_gitea_matrix_runner_uses_every_generated_entry_without_hardcoded_coordinates + runner = File.read(File.join(ROOT, "scripts/run_image_matrix_contract.sh")) + + assert_includes runner, 'matrix_json="$(ruby "$repo_root/scripts/matrix_json.rb")"' + assert_includes runner, 'for ((index = 0; index < entry_count; index++))' + assert_includes runner, 'bundle exec "$repo_root/scripts/run_image_contract.sh"' + assert_includes runner, '.image.reported_version == $version' + refute_match(/upstream-[0-9]/, runner) + refute_match(/ghcr\.io\/anomalyco/, runner) + end + def test_tuple_promotion_has_no_command_execution_or_deployment_client paths = %w[ lib/opencode_compat/runtime_tuple_promoter.rb