Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
17 commits
Select commit Hold shift + click to select a range
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
10 changes: 7 additions & 3 deletions .github/workflows/agents-keepalive-branch-sync.yml
Original file line number Diff line number Diff line change
Expand Up @@ -48,9 +48,11 @@ jobs:
steps:
- name: Validate fork inputs
id: validate
env:
HEAD_IS_FORK: ${{ inputs.head_is_fork || 'false' }}
HEAD_REPOSITORY: ${{ inputs.head_repository }}
run: |
if [ "${{ inputs.head_is_fork || 'false' }}" = "true" ] && \
[ -z "${{ inputs.head_repository }}" ]; then
if [ "$HEAD_IS_FORK" = "true" ] && [ -z "$HEAD_REPOSITORY" ]; then
{
echo "## Keepalive branch sync";
echo "";
Expand All @@ -62,8 +64,10 @@ jobs:
fi

- name: Warn when head repository is missing
env:
HEAD_REPOSITORY: ${{ inputs.head_repository }}
run: |
if [ -z "${{ inputs.head_repository }}" ]; then
if [ -z "$HEAD_REPOSITORY" ]; then
echo "::warning::Head repository missing;"
echo "::warning::Defaulting to base repository for checkout."
fi
Expand Down
12 changes: 8 additions & 4 deletions .github/workflows/maint-45-cosmetic-repair.yml
Original file line number Diff line number Diff line change
Expand Up @@ -63,18 +63,22 @@ jobs:
exit 0

- name: Run cosmetic repair script
env:
DRY_RUN: ${{ inputs['dry-run'] }}
BRANCH_SUFFIX: ${{ inputs['branch-suffix'] }}
BASE_BRANCH: ${{ github.event.repository.default_branch }}
run: |
set -euo pipefail
cmd=("python" "scripts/ci_cosmetic_repair.py")
if [ "${{ inputs['dry-run'] }}" = "true" ]; then
if [ "$DRY_RUN" = "true" ]; then
cmd+=("--dry-run" "--skip-pr")
else
cmd+=("--apply")
fi
if [ -n "${{ inputs['branch-suffix'] }}" ]; then
cmd+=("--branch-suffix" "${{ inputs['branch-suffix'] }}")
if [ -n "$BRANCH_SUFFIX" ]; then
cmd+=("--branch-suffix" "$BRANCH_SUFFIX")
fi
cmd+=("--base" "${{ github.event.repository.default_branch }}")
cmd+=("--base" "$BASE_BRANCH")
echo "Running: ${cmd[*]}"
"${cmd[@]}"

Expand Down
4 changes: 3 additions & 1 deletion .github/workflows/maint-72-fix-pr-body-conflicts.yml
Original file line number Diff line number Diff line change
Expand Up @@ -104,8 +104,10 @@ jobs:
steps:
- name: Check if should process this repo
id: check
env:
TARGET_REPO: ${{ inputs.target_repo }}
run: |
TARGET="${{ inputs.target_repo }}"
TARGET="$TARGET_REPO"
CURRENT="${{ matrix.repo }}"
if [ -n "$TARGET" ] && [ "$TARGET" != "$CURRENT" ]; then
echo "skip=true" >> "$GITHUB_OUTPUT"
Expand Down
16 changes: 11 additions & 5 deletions .github/workflows/maint-82-sync-dependency-campaign.yml
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,12 @@ jobs:
- name: Refresh campaign issue
id: campaign
uses: actions/github-script@v9
env:
REGISTERED_REPOS: ${{ steps.registered.outputs.repos }}
REQUESTED_REPOS: ${{ steps.inputs.outputs.repos }}
CAMPAIGN_DRY_RUN: ${{ steps.inputs.outputs.dry_run }}
CAMPAIGN_MAX_REPOS: ${{ steps.inputs.outputs.max_repos }}
CURRENT_SYNC_HASH: ${{ steps.hash.outputs.hash }}
with:
github-token: ${{ env.CAMPAIGN_TOKEN }}
script: |
Expand All @@ -102,17 +108,17 @@ jobs:
} = require('./.github/scripts/sync_dependency_campaign.js');
const fs = require('fs');

const registeredRepos = '${{ steps.registered.outputs.repos }}'
const registeredRepos = process.env.REGISTERED_REPOS
.split(',')
.map((repo) => repo.trim())
.filter(Boolean);
const requestedRepos = '${{ steps.inputs.outputs.repos }}';
const requestedRepos = process.env.REQUESTED_REPOS;
const repos = !requestedRepos || requestedRepos === 'all'
? registeredRepos
: requestedRepos.split(',').map((repo) => repo.trim()).filter(Boolean);
const dryRun = '${{ steps.inputs.outputs.dry_run }}' === 'true';
const maxRepos = Number('${{ steps.inputs.outputs.max_repos }}') || 0;
const currentSyncHash = '${{ steps.hash.outputs.hash }}';
const dryRun = process.env.CAMPAIGN_DRY_RUN === 'true';
const maxRepos = Number(process.env.CAMPAIGN_MAX_REPOS) || 0;
const currentSyncHash = process.env.CURRENT_SYNC_HASH;
const deliveryHandoffRecords =
context.payload.client_payload?.delivery_handoff_records || [];

Expand Down
14 changes: 10 additions & 4 deletions .github/workflows/reusable-10-ci-python.yml
Original file line number Diff line number Diff line change
Expand Up @@ -1998,13 +1998,16 @@ jobs:
id: coverage_min
if: ${{ inputs.coverage && inputs['coverage-min'] != '' }}
continue-on-error: true
env:
COVERAGE_MIN: ${{ inputs['coverage-min'] }}
run: |
python - <<'PY'
import os
import sys
import xml.etree.ElementTree as ET
from pathlib import Path

target = float("${{ inputs['coverage-min'] }}")
target = float(os.environ["COVERAGE_MIN"])
Comment thread
stranske marked this conversation as resolved.
path = Path("coverage.xml")
if not path.is_file():
print("coverage.xml not found", file=sys.stderr)
Expand Down Expand Up @@ -2216,6 +2219,7 @@ jobs:
PYTEST_OUTCOME: ${{ steps.pytest.outcome || 'skipped' }}
COVERAGE_MIN_OUTCOME: ${{ steps.coverage_min.outcome || 'skipped' }}
COVERAGE_ENABLED: ${{ inputs.coverage }}
COVERAGE_MIN: ${{ inputs['coverage-min'] }}
run: |
set -euo pipefail
failures=()
Expand Down Expand Up @@ -2244,7 +2248,7 @@ jobs:
}

record_outcome "pytest" "${PYTEST_OUTCOME}"
if [ "${{ inputs.coverage && inputs['coverage-min'] != '' }}" = 'true' ]; then
if [ "$COVERAGE_ENABLED" = 'true' ] && [ -n "$COVERAGE_MIN" ]; then
record_outcome "coverage minimum" "${COVERAGE_MIN_OUTCOME}"
fi

Expand Down Expand Up @@ -2416,11 +2420,13 @@ jobs:
&& inputs['enable-soft-gate']
&& matrix.python-version == env.PRIMARY_PYTHON_VERSION
}}
env:
COVERAGE_MIN: ${{ inputs['coverage-min'] }}
run: |
# shellcheck disable=SC2086
MIN_ARG=""
if [ -n "${{ inputs['coverage-min'] }}" ]; then
MIN_ARG="--minimum ${{ inputs['coverage-min'] }}"
if [ -n "$COVERAGE_MIN" ]; then
MIN_ARG="--minimum $COVERAGE_MIN"
fi
# shellcheck disable=SC2086
python "${GITHUB_WORKSPACE}/tools/coverage_trend.py" \
Expand Down
12 changes: 10 additions & 2 deletions .github/workflows/reusable-18-autofix.yml
Original file line number Diff line number Diff line change
Expand Up @@ -152,7 +152,15 @@ jobs:
steps:
- name: Set commit prefix env
id: prefix
run: echo "AUTOFIX_COMMIT_PREFIX=${{ inputs.commit_prefix }}" >> "$GITHUB_ENV"
env:
COMMIT_PREFIX: ${{ inputs.commit_prefix }}
run: |
set -euo pipefail
if [[ "$COMMIT_PREFIX" == *$'\n'* || "$COMMIT_PREFIX" == *$'\r'* ]]; then
echo "::error::commit_prefix must be a single-line value"
exit 1
fi
printf 'AUTOFIX_COMMIT_PREFIX=%s\n' "$COMMIT_PREFIX" >> "$GITHUB_ENV"
- name: Set sanitized PR head ref
run: |
set -euo pipefail
Expand Down Expand Up @@ -321,7 +329,7 @@ jobs:
if [ "$caller" = "github-actions" ] || [ "$caller" = "github-actions[bot]" ]; then
head_msg="$(git log -1 --pretty=%s 2>/dev/null || echo '')"
head_msg_lc=$(printf '%s' "$head_msg" | tr '[:upper:]' '[:lower:]')
prefix_lc=$(printf '%s' "${{ inputs.commit_prefix }}" | tr '[:upper:]' '[:lower:]')
prefix_lc=$(printf '%s' "$AUTOFIX_COMMIT_PREFIX" | tr '[:upper:]' '[:lower:]')
if [ -n "$prefix_lc" ]; then
msg_prefix=$(printf '%.*s' "${#prefix_lc}" "$head_msg_lc")
if [ "$msg_prefix" = "$prefix_lc" ]; then
Expand Down
56 changes: 56 additions & 0 deletions docs/workflows/script-interpolation-triage.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
# Workflow script interpolation triage (#3016)

Follow-up to merged PR #3020. This documents the repository-wide review of
`${{ inputs.* }}` and `${{ github.event.* }}` expressions that appear inside
`run:` or `with.script:` block scalars.

## Summary

| Category | Count | Action |
| --- | ---: | --- |
| Free-text inputs moved to `env:` in #3020 | 3 fields | `commit_message`, `codex_args`, `repos` |
| Additional free-text fixes in this PR | 4 fields | `target_repo`, `commit_prefix`, `head_repository`, campaign script outputs |
| Reviewed constrained interpolations | 103 | Explicit allowlist in `test_no_untrusted_interpolation.py` |

## Free-text inputs (must use `env:` indirection)

These workflow-dispatch or runner inputs are free-form text and must never
appear directly inside a `run:`/`script:` scalar:

- `inputs.commit_message` — fixed in #3020 (`maint-70`)
- `inputs.codex_args` — fixed in #3020 (`reusable-codex-run`)
- `inputs.repos` — fixed in #3020 (maint/health sync workflows)
- `inputs.target_repo` — fixed here (`maint-72`)
- `inputs.commit_prefix` — fixed here (`reusable-18-autofix`)
- `inputs.head_repository` — fixed here (`agents-keepalive-branch-sync`)

The regression guard bans these expressions in script bodies and fails if they
reappear.

## Constrained-value allowlist

The remaining 103 `(workflow, step, expression)` tuples are provably
constrained:

- **Booleans / dry-run flags** — `inputs.dry_run`, `inputs.create_issue`, etc.
- **Numeric identifiers** — `inputs.pr_number`, `inputs.issue_number`, `github.event.issue.number`
- **Repo-controlled refs** — `github.event.pull_request.base.ref`, `github.event.repository.default_branch`
- **Enumerated modes** — `inputs.mode`, `inputs.agent_key`, `inputs.provider`, `inputs.package-manager`, `inputs.test-runner`
- **Step output passthrough** — `steps.registered.outputs.repos` consumed via step `env:` in `maint-82`

Each tuple is recorded in `REVIEWED_SCRIPT_INTERPOLATIONS` inside
`tests/workflows/test_no_untrusted_interpolation.py`. Adding a new
`inputs.*`/`github.event.*` script interpolation requires updating that set and
this document.

## Test gate

`tests/workflows/test_no_untrusted_interpolation.py::test_no_untrusted_expressions_in_script_bodies`
parses every workflow file, walks `run:`/`script:` scalars, and asserts:

1. Banned free-text expressions do not appear.
2. Every other `inputs.*`/`github.event.*` hit is present in the reviewed allowlist.

Deliberate-break check (from issue acceptance criteria): re-introduce
`git commit -m "${{ inputs.commit_message }}"` in `maint-70` and confirm the
named test fails.
16 changes: 0 additions & 16 deletions langsmith-fleet-worker-attempt.json

This file was deleted.

12 changes: 9 additions & 3 deletions tests/workflows/test_maint82_sync_campaign_contract.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,9 +24,15 @@ def test_campaign_refresh_summary_publishes_source_sync_contract():


def test_campaign_refresh_passes_current_sync_hash_to_runner():
script = _refresh_script()

assert "const currentSyncHash = '${{ steps.hash.outputs.hash }}';" in script
data = yaml.safe_load(WORKFLOW.read_text(encoding="utf-8"))
refresh_step = next(
step for step in data["jobs"]["campaign"]["steps"] if step.get("id") == "campaign"
)
script = refresh_step["with"]["script"]
env = refresh_step.get("env", {})

assert env["CURRENT_SYNC_HASH"] == "${{ steps.hash.outputs.hash }}"
assert "const currentSyncHash = process.env.CURRENT_SYNC_HASH;" in script
assert "currentSyncHash," in script


Expand Down
Loading
Loading