Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 4 additions & 2 deletions .castiron.stats.yml
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
schema_version: 1
generation_id: 2e672b14-22c2-4e3e-b20d-2cdd0d719583
generation_id: 9b8417bf-2647-4e27-96c4-ab0c8a05802c
openapi_spec_hash: dd725fb7d43ceec7fb2de6f8713d14b6
openapi_transformed_spec_hash: 10930179c5f116288e24e0c6fda46559
config_hash: 85382dd94c503b5d225adc7636a77c9f
codegen_sha: 2ee3a8abe63e13ed0fc033198d9b99b51d168ad3
codegen_sha: f49e4167225279058315dd0dd9a61eff5452dbb7
codegen_hash: 1ac4f2acbe408437949f44939424a321f147482ec0c3fe8c1afa81a2dabcbc77
public_codegen_sha: e9ad1cca7dcacf4f73b6daf228c07e4fe029ac88
138 changes: 138 additions & 0 deletions .github/workflows/castiron-custom-code.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,138 @@
# File generated from our OpenAPI spec by Castiron. See CONTRIBUTING.md for details.
name: Castiron custom code

on:
pull_request:
types: [opened, synchronize, reopened, ready_for_review]

permissions:
contents: read

# These experimental refs are restricted to trusted SDK maintainers.
# The workflow definition is trusted because its branch writers are trusted.
concurrency:
group: castiron-custom-code-${{ github.event.pull_request.number }}
cancel-in-progress: false

env:
REPORTER_SHA256: 2855964a3b73aa57fd5a6b668c98b4aeb5dc54fc73a227091f832ca13a1f1f04

jobs:
report:
name: Castiron / baseline consistency
if: github.event.pull_request.head.repo.full_name == github.repository && contains(fromJSON(vars.CASTIRON_CUSTOM_CODE_BRANCHES || '[]'), github.event.pull_request.head.ref)
runs-on: ubuntu-latest
timeout-minutes: 10
outputs:
trusted: ${{ steps.trust.outputs.trusted }}
steps:
- name: Check the exact protected branch name
id: trust
env:
ALLOWED_BRANCHES: ${{ vars.CASTIRON_CUSTOM_CODE_BRANCHES || '[]' }}
PR_BRANCH: ${{ github.event.pull_request.head.ref }}
run: |
jq -e --arg branch "$PR_BRANCH" 'type == "array" and index($branch) != null' <<< "$ALLOWED_BRANCHES" > /dev/null
printf 'trusted=true\n' >> "$GITHUB_OUTPUT"

- name: Check out the protected test branch
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
with:
ref: ${{ github.event.pull_request.head.sha }}
fetch-depth: 0

- name: Verify the reviewed reporter
run: printf '%s %s\n' "$REPORTER_SHA256" scripts/castiron/custom_code_report.py | sha256sum --check --strict

- name: Test hash mismatch and snapshot isolation
run: python3 scripts/castiron/test_custom_code_report.py

- name: Validate the codegen hash and report custom code
env:
BASE_SHA: ${{ github.event.pull_request.base.sha }}
HEAD_SHA: ${{ github.event.pull_request.head.sha }}
PUBLIC_REPOSITORY: ${{ !github.event.repository.private }}
run: |
git fetch --quiet --no-tags origin "$BASE_SHA" "$HEAD_SHA"
mode=()
if [[ "$PUBLIC_REPOSITORY" == true ]]; then mode=(--public); fi
python3 -I scripts/castiron/custom_code_report.py report \
--base "$BASE_SHA" --head "$HEAD_SHA" \
--fetch --require-head-hash "${mode[@]}" \
--out "$RUNNER_TEMP/castiron-custom-code"

- name: Add the report to the run summary
if: always()
run: |
if test -f "$RUNNER_TEMP/castiron-custom-code/summary.md"; then
cat "$RUNNER_TEMP/castiron-custom-code/summary.md" >> "$GITHUB_STEP_SUMMARY"
fi

- name: Upload report and current custom-code patch
if: always()
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
name: castiron-custom-code-${{ github.run_id }}-${{ github.run_attempt }}
path: ${{ runner.temp }}/castiron-custom-code/
if-no-files-found: error
retention-days: 7

comment:
name: Update custom-code comment
needs: report
if: always() && !cancelled() && needs.report.outputs.trusted == 'true'
runs-on: ubuntu-latest
timeout-minutes: 5
permissions:
contents: read
actions: read
pull-requests: write
steps:
- name: Check out the protected test branch
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
with:
ref: ${{ github.event.pull_request.head.sha }}
persist-credentials: false

- name: Verify the reviewed publisher before execution
run: printf '%s %s\n' "$REPORTER_SHA256" scripts/castiron/custom_code_report.py | sha256sum --check --strict

- name: Download this run's report
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
with:
name: castiron-custom-code-${{ github.run_id }}-${{ github.run_attempt }}
path: ${{ runner.temp }}/castiron-custom-code

- name: Create or update the single report comment
id: publish
env:
GH_TOKEN: ${{ github.token }}
REPOSITORY: ${{ github.repository }}
PR_NUMBER: ${{ github.event.pull_request.number }}
RUN_ID: ${{ github.run_id }}
RUN_ATTEMPT: ${{ github.run_attempt }}
run: |
python3 -I scripts/castiron/custom_code_report.py comment \
--report "$RUNNER_TEMP/castiron-custom-code/report.json" \
--repository "$REPOSITORY" --pr "$PR_NUMBER" --run-id "$RUN_ID" \
--run-attempt "$RUN_ATTEMPT"

- name: Publish a trusted failure status
if: always() && !cancelled() && steps.publish.outcome != 'success'
uses: actions/github-script@f28e40c7f34bde8b3046d885e986cb6290c5673b # v7
with:
script: |
const marker = '<!-- castiron:custom-code-report:v1 -->';
const event = context.payload.pull_request;
const {data: current} = await github.rest.pulls.get({...context.repo, pull_number: event.number});
if (current.state !== 'open' || current.head.sha !== event.head.sha || current.base.sha !== event.base.sha) return;
const comments = await github.paginate(github.rest.issues.listComments, {...context.repo, issue_number: event.number});
const previous = comments.find(c => c.user?.type === 'Bot' && c.user?.login === 'github-actions[bot]' && c.body?.includes(marker));
const run = Number(context.runId);
const attempt = Number(process.env.GITHUB_RUN_ATTEMPT);
const prior = previous?.body?.match(/<!-- castiron:run:v1:(\d+):(\d+) -->/);
if (prior && (Number(prior[1]) > run || (Number(prior[1]) === run && Number(prior[2]) > attempt))) return;
const url = `${context.serverUrl}/${context.repo.owner}/${context.repo.repo}/actions/runs/${run}`;
const body = `${marker}\n\n## Castiron custom code\n\n⚠️ Report unavailable for \`${event.head.sha.slice(0, 12)}\`.\n\nThe report setup or validation failed. [Inspect the workflow run](${url}).\n\n<!-- castiron:run:v1:${run}:${attempt} -->`;
if (previous) await github.rest.issues.updateComment({...context.repo, comment_id: previous.id, body});
else await github.rest.issues.createComment({...context.repo, issue_number: event.number, body});
23 changes: 23 additions & 0 deletions scripts/castiron/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
<!-- File generated from our OpenAPI spec by Castiron. See CONTRIBUTING.md for details. -->
# Castiron custom-code reporting

Castiron maintains shared templates for these files. Prefer changing those templates
for cross-SDK improvements; repository-specific customizations use the normal
three-way merge and are allowed.
The reporter uses Python 3.10+, Git, and `gh`; it does not import SDK code.

Run `python3 scripts/castiron/test_custom_code_report.py` for focused tests.
The report comment includes commands to inspect the exact custom-code patch.
Public reporting uses only public snapshots and needs no private repository access.

The workflow validates the recorded `codegen_hash`.
Its hash format is documented in the reporter. Only `.github/actions/` and
`.github/workflows/` are excluded from the content hash.

During the draft rollout, `CASTIRON_CUSTOM_CODE_BRANCHES` is a repository-local
JSON array of maintainer-protected branch names. Only those branches can publish
comments. A general contributor/fork rollout requires a trusted default-branch
publisher. Never execute PR-controlled code with write credentials.
Changing the workflow may require one-time AM permission. Its reporter checksum
is a credential-safety check, not a requirement that every generated file remain
identical to its template.
Loading
Loading