From aa501763bb9dd9759b2976aa8a9191af8a3294cd Mon Sep 17 00:00:00 2001 From: Nikolay Petrov Date: Fri, 30 Oct 2026 21:52:02 -0700 Subject: [PATCH 01/22] feat(action): composite setup action; repo-eval uses it (part of #318) action/setup installs dokimi at a pinned commit (SHA, or a tag resolved to its commit; a branch is refused) with the locked dependencies, sets up Python, and exports CI=true, DOKIMI_RUNS_DIR and, with max-usd, DOKIMI_MAX_USD. The output directory has a fixed layout (runs/) for upload-artifact. A paid job (paid: true) must pass max-usd; a free job needs none. repo-eval.yml moves its ref resolution and install into the action; its inputs, outputs and permissions are unchanged. The action is read from the called workflow's own commit (job.workflow_sha), so external callers need no checkout of their own, and it is removed again after the setup step. zizmor covers action/ in CI and pre-commit; repo-eval-self runs on action/ changes. Signed-off-by: Nikolay Petrov --- .github/workflows/ci.yml | 2 +- .github/workflows/repo-eval-self.yml | 3 +- .github/workflows/repo-eval.yml | 108 +++++++++---------- .pre-commit-config.yaml | 4 +- action/setup/action.yml | 153 +++++++++++++++++++++++++++ 5 files changed, 213 insertions(+), 57 deletions(-) create mode 100644 action/setup/action.yml diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index dc4abe517..b733390a7 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -90,7 +90,7 @@ jobs: - name: zizmor if: ${{ !cancelled() }} working-directory: . - run: zizmor --offline --min-severity medium .github/workflows + run: zizmor --offline --min-severity medium .github/workflows action # Every tracked file carries the SPDX license header; scope and exceptions # are in .github/.licenserc.yaml. A `uses:` step, so `make ci-local` skips # it (the config's header comment has the docker command for a local run). diff --git a/.github/workflows/repo-eval-self.yml b/.github/workflows/repo-eval-self.yml index ed6d75a02..3bfcc72f0 100644 --- a/.github/workflows/repo-eval-self.yml +++ b/.github/workflows/repo-eval-self.yml @@ -23,6 +23,7 @@ on: - 'dokimi/pyproject.toml' - '.github/workflows/repo-eval.yml' - '.github/workflows/repo-eval-self.yml' + - 'action/**' workflow_dispatch: inputs: tasks: @@ -51,7 +52,7 @@ jobs: # Every `paths:` entry above outside .agent-eval/, as a prefix. smoke-paths: >- dokimi/dokimi/repos/ dokimi/dokimi/commands/repo dokimi/dokimi/schemas/repo_ - dokimi/requirements.lock.txt dokimi/pyproject.toml .github/workflows/repo-eval.yml + dokimi/requirements.lock.txt dokimi/pyproject.toml .github/workflows/repo-eval.yml action/ max-parallel: 4 # intel-ubuntu-latest: see ci.yml. runs-on: ${{ github.repository_owner == 'intel' && 'intel-ubuntu-latest' || 'ubuntu-latest' }} diff --git a/.github/workflows/repo-eval.yml b/.github/workflows/repo-eval.yml index a44766dc0..5c7d22e46 100644 --- a/.github/workflows/repo-eval.yml +++ b/.github/workflows/repo-eval.yml @@ -107,7 +107,7 @@ jobs: permissions: contents: read outputs: - dokimi-sha: ${{ steps.ref.outputs.sha }} + dokimi-sha: ${{ steps.dokimi.outputs.sha }} id: ${{ steps.select.outputs.id }} tasks: ${{ steps.select.outputs.tasks }} steps: @@ -117,42 +117,30 @@ jobs: with: persist-credentials: false fetch-depth: 0 - - uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0 + # The setup action comes from this workflow's own commit (job.workflow_sha), + # the one the caller's uses: line pins, and installs dokimi-ref. Sparse: its action/ only. + - name: Check out the setup action + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: + repository: ${{ inputs.dokimi-repository }} + ref: ${{ job.workflow_sha }} + path: .dokimi-action + sparse-checkout: /action/ + sparse-checkout-cone-mode: false + persist-credentials: false + # Resolves dokimi-ref to a commit (the validate outputs pass it on) and installs dokimi. + - name: Set up dokimi + id: dokimi + uses: ./.dokimi-action/action/setup + with: + dokimi-ref: ${{ inputs.dokimi-ref }} + dokimi-repository: ${{ inputs.dokimi-repository }} python-version: '3.13' - # A SHA is taken as it is; a tag is resolved to the commit it names, so the - # later jobs install the same commit even if the tag moves meanwhile. - - name: Resolve dokimi-ref - id: ref - env: - REF: ${{ inputs.dokimi-ref }} - DOKIMI_REPO: ${{ inputs.dokimi-repository }} - run: | - if ! [[ "$DOKIMI_REPO" =~ ^[A-Za-z0-9-]+/[A-Za-z0-9._-]+$ ]]; then - echo "::error title=dokimi-repository::dokimi-repository is not owner/name" - exit 1 - fi - if [[ "$REF" =~ ^[0-9a-f]{40}$ ]]; then - sha=$REF - elif [[ "$REF" =~ ^[A-Za-z0-9][A-Za-z0-9._-]*$ ]]; then - # An annotated tag's commit is its peeled (^{}) line. - sha=$(git ls-remote --tags "https://github.com/$DOKIMI_REPO" \ - | awk -v t="refs/tags/$REF" '$2 == t "^{}" { p = $1 } $2 == t { l = $1 } END { print (p != "" ? p : l) }') - fi - if ! [[ "${sha:-}" =~ ^[0-9a-f]{40}$ ]]; then - echo "::error title=dokimi-ref::dokimi-ref is neither a 40-digit commit SHA nor a tag of $DOKIMI_REPO" - exit 1 - fi - echo "sha=$sha" >> "$GITHUB_OUTPUT" - echo "dokimi $DOKIMI_REPO@$REF -> $sha" - - name: Install dokimi - env: - SHA: ${{ steps.ref.outputs.sha }} - DOKIMI_REPO: ${{ inputs.dokimi-repository }} - run: | - # The lock is hashed, and pip cannot hash a git URL: install the lock, then dokimi without deps. - pip install --require-hashes -r "https://raw.githubusercontent.com/$DOKIMI_REPO/$SHA/dokimi/requirements.lock.txt" - pip install --no-deps "dokimi @ git+https://github.com/$DOKIMI_REPO@$SHA#subdirectory=dokimi" + # The checkout is only needed to run the setup action: remove it, so the + # caller's tree and its image build context are what they were. + - name: Remove the setup action checkout + shell: bash + run: rm -rf .dokimi-action - name: repo validate env: MANIFEST: ${{ inputs.manifest }} @@ -294,17 +282,24 @@ jobs: - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: persist-credentials: false - - uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0 + - name: Check out the setup action + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + repository: ${{ inputs.dokimi-repository }} + ref: ${{ job.workflow_sha }} + path: .dokimi-action + sparse-checkout: /action/ + sparse-checkout-cone-mode: false + persist-credentials: false + - name: Set up dokimi + uses: ./.dokimi-action/action/setup with: + dokimi-ref: ${{ needs.validate.outputs.dokimi-sha }} + dokimi-repository: ${{ inputs.dokimi-repository }} python-version: '3.13' - - name: Install dokimi - env: - SHA: ${{ needs.validate.outputs.dokimi-sha }} - DOKIMI_REPO: ${{ inputs.dokimi-repository }} - run: | - # The lock is hashed, and pip cannot hash a git URL: install the lock, then dokimi without deps. - pip install --require-hashes -r "https://raw.githubusercontent.com/$DOKIMI_REPO/$SHA/dokimi/requirements.lock.txt" - pip install --no-deps "dokimi @ git+https://github.com/$DOKIMI_REPO@$SHA#subdirectory=dokimi" + - name: Remove the setup action checkout + shell: bash + run: rm -rf .dokimi-action # Report only: a failing claim does not fail the job (an unreadable # manifest still does). The table lists the failing claims. - name: repo scan @@ -362,17 +357,24 @@ jobs: with: persist-credentials: false fetch-depth: 0 - - uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0 + - name: Check out the setup action + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: + repository: ${{ inputs.dokimi-repository }} + ref: ${{ job.workflow_sha }} + path: .dokimi-action + sparse-checkout: /action/ + sparse-checkout-cone-mode: false + persist-credentials: false + - name: Set up dokimi + uses: ./.dokimi-action/action/setup + with: + dokimi-ref: ${{ needs.validate.outputs.dokimi-sha }} + dokimi-repository: ${{ inputs.dokimi-repository }} python-version: '3.13' - - name: Install dokimi - env: - SHA: ${{ needs.validate.outputs.dokimi-sha }} - DOKIMI_REPO: ${{ inputs.dokimi-repository }} - run: | - # The lock is hashed, and pip cannot hash a git URL: install the lock, then dokimi without deps. - pip install --require-hashes -r "https://raw.githubusercontent.com/$DOKIMI_REPO/$SHA/dokimi/requirements.lock.txt" - pip install --no-deps "dokimi @ git+https://github.com/$DOKIMI_REPO@$SHA#subdirectory=dokimi" + - name: Remove the setup action checkout + shell: bash + run: rm -rf .dokimi-action # Builds the image from the manifest's Dockerfile (the tree without its # tasks), then grades the null and the oracle workspace in it. - name: repo tasks check diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 270733710..6c1653ff2 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -45,10 +45,10 @@ repos: - repo: https://github.com/zizmorcore/zizmor-pre-commit rev: c92afdf2c6ca336aaa8f39e2bd8d48f62dbd8e1f # frozen: v1.31.1 hooks: - # The static job's zizmor step: the same command and scope. + # The static job's zizmor step: the same command and scope (workflows and the composite actions). - id: zizmor args: [--no-progress, --offline, --min-severity, medium] - files: ^\.github/workflows/[^/]+\.ya?ml$ + files: ^(\.github/workflows/[^/]+|action/[^/]+/action)\.ya?ml$ - repo: https://github.com/gitleaks/gitleaks rev: 83d9cd684c87d95d656c1458ef04895a7f1cbd8e # frozen: v8.30.1 diff --git a/action/setup/action.yml b/action/setup/action.yml new file mode 100644 index 000000000..2e95b45d8 --- /dev/null +++ b/action/setup/action.yml @@ -0,0 +1,153 @@ +# Copyright (C) 2026 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 + +name: dokimi setup +description: >- + Installs dokimi at one pinned commit (a SHA, or a release tag resolved to its + commit; a branch is refused) with its locked dependencies, sets up Python, and + exports what a dokimi run expects on CI: CI=true, DOKIMI_RUNS_DIR and, with + `max-usd`, DOKIMI_MAX_USD. Needs no secret. Callers grant `contents: read` at most. + +inputs: + dokimi-ref: + description: >- + dokimi commit SHA (40 hex digits) or release tag to install. Use the ref of the + `uses:` line that pins this action; a branch is refused. + required: true + dokimi-repository: + description: Repository dokimi-ref is in (owner/name), for testing a fork + required: false + default: intel/dokimi + python-version: + description: Python the install and the checks run under + required: false + default: '3.13' + out-dir: + description: >- + Directory for the run output, created by this action; empty means + `$RUNNER_TEMP/dokimi-out`. Its layout is fixed: `runs/` is the runs root + (DOKIMI_RUNS_DIR). Upload the whole directory as an artifact. + required: false + default: '' + cache: + description: >- + `true` restores and saves pip's download cache. The key is the resolved commit, so a + changed lock is a new commit and a new key. + required: false + default: 'false' + paid: + description: >- + `true` when a step of this job calls a model and spends money. Then `max-usd` is + required. Free steps leave it `false` and need no cap. + required: false + default: 'false' + max-usd: + description: >- + The cap in US dollars on the whole job, exported as DOKIMI_MAX_USD. Required when + `paid` is `true`; a value that is not a positive number fails the job. + required: false + default: '' + +outputs: + sha: + description: The commit installed (40 hex digits) + value: ${{ steps.ref.outputs.sha }} + out-dir: + description: Absolute path of the output directory, to upload + value: ${{ steps.out.outputs.dir }} + +runs: + using: composite + steps: + - name: Check the inputs + shell: bash + env: + PAID: ${{ inputs.paid }} + MAX_USD: ${{ inputs.max-usd }} + run: | + case "$PAID" in + true|false) ;; + *) echo "::error title=paid::paid must be 'true' or 'false', not '$PAID'"; exit 1 ;; + esac + if [ -n "$MAX_USD" ]; then + if ! [[ "$MAX_USD" =~ ^[0-9]+(\.[0-9]+)?$ ]] || ! awk -v v="$MAX_USD" 'BEGIN { exit !(v > 0) }'; then + echo "::error title=max-usd::max-usd must be a positive number of US dollars, not '$MAX_USD'" + exit 1 + fi + elif [ "$PAID" = true ]; then + echo "::error title=max-usd::max-usd is required when paid is true: a step here calls a model, and a run without a dollar cap is refused" + exit 1 + fi + + # Resolves the ref before anything is installed: a tag becomes the commit it names + # (the peeled line of ls-remote), so the jobs of one run install the same commit. + - name: Resolve dokimi-ref + id: ref + shell: bash + env: + REF: ${{ inputs.dokimi-ref }} + DOKIMI_REPO: ${{ inputs.dokimi-repository }} + run: | + if ! [[ "$DOKIMI_REPO" =~ ^[A-Za-z0-9-]+/[A-Za-z0-9._-]+$ ]]; then + echo "::error title=dokimi-repository::dokimi-repository is not owner/name" + exit 1 + fi + if [[ "$REF" =~ ^[0-9a-f]{40}$ ]]; then + sha=$REF + elif [[ "$REF" =~ ^[A-Za-z0-9][A-Za-z0-9._-]*$ ]]; then + # An annotated tag's commit is its peeled (^{}) line. + sha=$(git ls-remote --tags "https://github.com/$DOKIMI_REPO" \ + | awk -v t="refs/tags/$REF" '$2 == t "^{}" { p = $1 } $2 == t { l = $1 } END { print (p != "" ? p : l) }') + fi + if ! [[ "${sha:-}" =~ ^[0-9a-f]{40}$ ]]; then + echo "::error title=dokimi-ref::dokimi-ref is neither a 40-digit commit SHA nor a tag of $DOKIMI_REPO" + exit 1 + fi + echo "sha=$sha" >> "$GITHUB_OUTPUT" + echo "dokimi $DOKIMI_REPO@$REF -> $sha" + + # The pip cache key is a file that names the commit: setup-python hashes it. + - name: Key for the pip cache + shell: bash + env: + SHA: ${{ steps.ref.outputs.sha }} + run: | + mkdir -p "$RUNNER_TEMP/dokimi-setup" + echo "dokimi $SHA" > "$RUNNER_TEMP/dokimi-setup/pip-key.txt" + + - name: Set up Python + uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0 + with: + python-version: ${{ inputs.python-version }} + cache: ${{ inputs.cache == 'true' && 'pip' || '' }} + cache-dependency-path: ${{ runner.temp }}/dokimi-setup/pip-key.txt + + - name: Install dokimi + shell: bash + env: + SHA: ${{ steps.ref.outputs.sha }} + DOKIMI_REPO: ${{ inputs.dokimi-repository }} + run: | + # The lock is hashed, and pip cannot hash a git URL: install the lock, then dokimi without deps. + pip install --require-hashes -r "https://raw.githubusercontent.com/$DOKIMI_REPO/$SHA/dokimi/requirements.lock.txt" + pip install --no-deps "dokimi @ git+https://github.com/$DOKIMI_REPO@$SHA#subdirectory=dokimi" + + - name: Output directory and environment + id: out + shell: bash + env: + OUT: ${{ inputs.out-dir }} + MAX_USD: ${{ inputs.max-usd }} + run: | # zizmor: ignore[github-env] -- intended: the two values written are checked above (no newline, a number) + out=${OUT:-$RUNNER_TEMP/dokimi-out} + mkdir -p "$out/runs" + dir=$(cd "$out" && pwd) + # Each value written to GITHUB_ENV is checked here too: a newline would add a variable. + if [[ "$dir" == *$'\n'* ]]; then echo "::error title=out-dir::out-dir has a newline"; exit 1; fi + if [ -n "$MAX_USD" ] && ! [[ "$MAX_USD" =~ ^[0-9]+(\.[0-9]+)?$ ]]; then + echo "::error title=max-usd::max-usd must be a positive number of US dollars"; exit 1 + fi + echo "dir=$dir" >> "$GITHUB_OUTPUT" + echo "CI=true" >> "$GITHUB_ENV" + echo "DOKIMI_RUNS_DIR=$dir/runs" >> "$GITHUB_ENV" + if [ -n "$MAX_USD" ]; then echo "DOKIMI_MAX_USD=$MAX_USD" >> "$GITHUB_ENV"; fi From 6cd5a9aa1e1ce7efff52d3f4c0410453e09e257f Mon Sep 17 00:00:00 2001 From: Nikolay Petrov Date: Fri, 30 Oct 2026 21:59:46 -0700 Subject: [PATCH 02/22] fix(action): keep the setup action checkout until the job ends (part of #318) The composite's post steps read its action.yml from .dokimi-action at job end. Removing the checkout after the setup step made "Post Set up dokimi" fail (CI run 41170348715, self / repo validate; scan and tasks-check were skipped behind it). The checkout is one sparse file tree, so it stays. Signed-off-by: Nikolay Petrov --- .github/workflows/repo-eval.yml | 12 +----------- 1 file changed, 1 insertion(+), 11 deletions(-) diff --git a/.github/workflows/repo-eval.yml b/.github/workflows/repo-eval.yml index 5c7d22e46..bf13f5052 100644 --- a/.github/workflows/repo-eval.yml +++ b/.github/workflows/repo-eval.yml @@ -119,6 +119,7 @@ jobs: fetch-depth: 0 # The setup action comes from this workflow's own commit (job.workflow_sha), # the one the caller's uses: line pins, and installs dokimi-ref. Sparse: its action/ only. + # Kept until the job ends: the composite's post steps read the action from this path (#319 CI). - name: Check out the setup action uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: @@ -136,11 +137,6 @@ jobs: dokimi-ref: ${{ inputs.dokimi-ref }} dokimi-repository: ${{ inputs.dokimi-repository }} python-version: '3.13' - # The checkout is only needed to run the setup action: remove it, so the - # caller's tree and its image build context are what they were. - - name: Remove the setup action checkout - shell: bash - run: rm -rf .dokimi-action - name: repo validate env: MANIFEST: ${{ inputs.manifest }} @@ -297,9 +293,6 @@ jobs: dokimi-ref: ${{ needs.validate.outputs.dokimi-sha }} dokimi-repository: ${{ inputs.dokimi-repository }} python-version: '3.13' - - name: Remove the setup action checkout - shell: bash - run: rm -rf .dokimi-action # Report only: a failing claim does not fail the job (an unreadable # manifest still does). The table lists the failing claims. - name: repo scan @@ -372,9 +365,6 @@ jobs: dokimi-ref: ${{ needs.validate.outputs.dokimi-sha }} dokimi-repository: ${{ inputs.dokimi-repository }} python-version: '3.13' - - name: Remove the setup action checkout - shell: bash - run: rm -rf .dokimi-action # Builds the image from the manifest's Dockerfile (the tree without its # tasks), then grades the null and the oracle workspace in it. - name: repo tasks check From 6ea3442a0f3db3e11a5ece9a539734d332fb863d Mon Sep 17 00:00:00 2001 From: Nikolay Petrov Date: Fri, 30 Oct 2026 22:22:33 -0700 Subject: [PATCH 03/22] ci(repo-eval): keep the setup-action comment to two lines The comment block above the validate job's setup-action checkout ran to three lines, which the comment-block check fails (AGENTS.md: two lines of reason, link the rest). Same content, shorter: the action comes from the workflow's commit, and it stays until the job ends because the composite's post steps read it. Signed-off-by: Nikolay Petrov --- .github/workflows/repo-eval.yml | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/.github/workflows/repo-eval.yml b/.github/workflows/repo-eval.yml index bf13f5052..3b8936314 100644 --- a/.github/workflows/repo-eval.yml +++ b/.github/workflows/repo-eval.yml @@ -117,9 +117,8 @@ jobs: with: persist-credentials: false fetch-depth: 0 - # The setup action comes from this workflow's own commit (job.workflow_sha), - # the one the caller's uses: line pins, and installs dokimi-ref. Sparse: its action/ only. - # Kept until the job ends: the composite's post steps read the action from this path (#319 CI). + # The setup action, from this workflow's own commit (job.workflow_sha), sparse to action/. + # Kept until the job ends: the composite's post steps read it from this path. - name: Check out the setup action uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: From c2c85558dd726391bb055f6b635c0567ce3d6d1b Mon Sep 17 00:00:00 2001 From: Nikolay Petrov Date: Fri, 30 Oct 2026 22:44:04 -0700 Subject: [PATCH 04/22] ci: move the setup action to actions/setup Follows the layout of abicheck: sub-actions live under actions//, and the public entry, if one is added later, is a root action.yml. Paths move in repo-eval.yml (sparse checkout and the uses line), repo-eval-self.yml (the path filter and smoke paths), ci.yml (the zizmor scope) and the pre-commit zizmor files regex. No behaviour change. Signed-off-by: Nikolay Petrov --- .github/workflows/ci.yml | 2 +- .github/workflows/repo-eval-self.yml | 4 ++-- .github/workflows/repo-eval.yml | 14 +++++++------- .pre-commit-config.yaml | 2 +- {action => actions}/setup/action.yml | 0 5 files changed, 11 insertions(+), 11 deletions(-) rename {action => actions}/setup/action.yml (100%) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index b733390a7..63c3be215 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -90,7 +90,7 @@ jobs: - name: zizmor if: ${{ !cancelled() }} working-directory: . - run: zizmor --offline --min-severity medium .github/workflows action + run: zizmor --offline --min-severity medium .github/workflows actions # Every tracked file carries the SPDX license header; scope and exceptions # are in .github/.licenserc.yaml. A `uses:` step, so `make ci-local` skips # it (the config's header comment has the docker command for a local run). diff --git a/.github/workflows/repo-eval-self.yml b/.github/workflows/repo-eval-self.yml index 3bfcc72f0..52b0f4b21 100644 --- a/.github/workflows/repo-eval-self.yml +++ b/.github/workflows/repo-eval-self.yml @@ -23,7 +23,7 @@ on: - 'dokimi/pyproject.toml' - '.github/workflows/repo-eval.yml' - '.github/workflows/repo-eval-self.yml' - - 'action/**' + - 'actions/**' workflow_dispatch: inputs: tasks: @@ -52,7 +52,7 @@ jobs: # Every `paths:` entry above outside .agent-eval/, as a prefix. smoke-paths: >- dokimi/dokimi/repos/ dokimi/dokimi/commands/repo dokimi/dokimi/schemas/repo_ - dokimi/requirements.lock.txt dokimi/pyproject.toml .github/workflows/repo-eval.yml action/ + dokimi/requirements.lock.txt dokimi/pyproject.toml .github/workflows/repo-eval.yml actions/ max-parallel: 4 # intel-ubuntu-latest: see ci.yml. runs-on: ${{ github.repository_owner == 'intel' && 'intel-ubuntu-latest' || 'ubuntu-latest' }} diff --git a/.github/workflows/repo-eval.yml b/.github/workflows/repo-eval.yml index 3b8936314..34fa72626 100644 --- a/.github/workflows/repo-eval.yml +++ b/.github/workflows/repo-eval.yml @@ -117,7 +117,7 @@ jobs: with: persist-credentials: false fetch-depth: 0 - # The setup action, from this workflow's own commit (job.workflow_sha), sparse to action/. + # The setup action, from this workflow's own commit (job.workflow_sha), sparse to actions/. # Kept until the job ends: the composite's post steps read it from this path. - name: Check out the setup action uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 @@ -125,13 +125,13 @@ jobs: repository: ${{ inputs.dokimi-repository }} ref: ${{ job.workflow_sha }} path: .dokimi-action - sparse-checkout: /action/ + sparse-checkout: /actions/ sparse-checkout-cone-mode: false persist-credentials: false # Resolves dokimi-ref to a commit (the validate outputs pass it on) and installs dokimi. - name: Set up dokimi id: dokimi - uses: ./.dokimi-action/action/setup + uses: ./.dokimi-action/actions/setup with: dokimi-ref: ${{ inputs.dokimi-ref }} dokimi-repository: ${{ inputs.dokimi-repository }} @@ -283,11 +283,11 @@ jobs: repository: ${{ inputs.dokimi-repository }} ref: ${{ job.workflow_sha }} path: .dokimi-action - sparse-checkout: /action/ + sparse-checkout: /actions/ sparse-checkout-cone-mode: false persist-credentials: false - name: Set up dokimi - uses: ./.dokimi-action/action/setup + uses: ./.dokimi-action/actions/setup with: dokimi-ref: ${{ needs.validate.outputs.dokimi-sha }} dokimi-repository: ${{ inputs.dokimi-repository }} @@ -355,11 +355,11 @@ jobs: repository: ${{ inputs.dokimi-repository }} ref: ${{ job.workflow_sha }} path: .dokimi-action - sparse-checkout: /action/ + sparse-checkout: /actions/ sparse-checkout-cone-mode: false persist-credentials: false - name: Set up dokimi - uses: ./.dokimi-action/action/setup + uses: ./.dokimi-action/actions/setup with: dokimi-ref: ${{ needs.validate.outputs.dokimi-sha }} dokimi-repository: ${{ inputs.dokimi-repository }} diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 6c1653ff2..f0df46567 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -48,7 +48,7 @@ repos: # The static job's zizmor step: the same command and scope (workflows and the composite actions). - id: zizmor args: [--no-progress, --offline, --min-severity, medium] - files: ^(\.github/workflows/[^/]+|action/[^/]+/action)\.ya?ml$ + files: ^(\.github/workflows/[^/]+|actions/[^/]+/action)\.ya?ml$ - repo: https://github.com/gitleaks/gitleaks rev: 83d9cd684c87d95d656c1458ef04895a7f1cbd8e # frozen: v8.30.1 diff --git a/action/setup/action.yml b/actions/setup/action.yml similarity index 100% rename from action/setup/action.yml rename to actions/setup/action.yml From 6c4a171821873535347d849df3e095440b36bc2a Mon Sep 17 00:00:00 2001 From: Nikolay Petrov Date: Sat, 31 Oct 2026 12:38:15 -0700 Subject: [PATCH 05/22] Address review: key the pip cache file under the workspace setup-python hashes cache-dependency-path with glob.hashFiles, which only sees files under GITHUB_WORKSPACE. The key file was in runner.temp, so a cache-enabled call hashed nothing and setup-python failed. The key file now sits in $GITHUB_WORKSPACE/.dokimi-setup, and cache-dependency-path points at it. The test pins both ends statically; no runner runs a cache-enabled job here. Thread: PRRT_kwDOVBg7g86ySzDB Signed-off-by: Nikolay Petrov --- actions/setup/action.yml | 9 ++++--- dokimi/tests/test_setup_action.py | 43 +++++++++++++++++++++++++++++++ 2 files changed, 48 insertions(+), 4 deletions(-) create mode 100644 dokimi/tests/test_setup_action.py diff --git a/actions/setup/action.yml b/actions/setup/action.yml index 2e95b45d8..d3316cbac 100644 --- a/actions/setup/action.yml +++ b/actions/setup/action.yml @@ -106,21 +106,22 @@ runs: echo "sha=$sha" >> "$GITHUB_OUTPUT" echo "dokimi $DOKIMI_REPO@$REF -> $sha" - # The pip cache key is a file that names the commit: setup-python hashes it. + # The pip cache key is a file that names the commit: setup-python hashes it. It sits in + # the workspace, since setup-python's hashFiles only sees files under GITHUB_WORKSPACE. - name: Key for the pip cache shell: bash env: SHA: ${{ steps.ref.outputs.sha }} run: | - mkdir -p "$RUNNER_TEMP/dokimi-setup" - echo "dokimi $SHA" > "$RUNNER_TEMP/dokimi-setup/pip-key.txt" + mkdir -p "$GITHUB_WORKSPACE/.dokimi-setup" + echo "dokimi $SHA" > "$GITHUB_WORKSPACE/.dokimi-setup/pip-key.txt" - name: Set up Python uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0 with: python-version: ${{ inputs.python-version }} cache: ${{ inputs.cache == 'true' && 'pip' || '' }} - cache-dependency-path: ${{ runner.temp }}/dokimi-setup/pip-key.txt + cache-dependency-path: ${{ github.workspace }}/.dokimi-setup/pip-key.txt - name: Install dokimi shell: bash diff --git a/dokimi/tests/test_setup_action.py b/dokimi/tests/test_setup_action.py new file mode 100644 index 000000000..8015d4a31 --- /dev/null +++ b/dokimi/tests/test_setup_action.py @@ -0,0 +1,43 @@ +# Copyright (C) 2026 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 + +"""actions/setup/action.yml: the pip cache key must sit where setup-python can hash it. + +setup-python hashes ``cache-dependency-path`` with ``@actions/glob.hashFiles``, which only +sees files under GITHUB_WORKSPACE. A key file in ``runner.temp`` hashes to nothing, and +setup-python then fails with no dependency file matched. The check reads the composite's +steps and pins that the key is written under the workspace and that setup-python is given +the same path. +""" + +from __future__ import annotations + +from pathlib import Path + +import yaml + +ACTION = Path(__file__).resolve().parents[2] / "actions/setup/action.yml" +KEY_FILE = ".dokimi-setup/pip-key.txt" + + +def _steps() -> list[dict]: + action = yaml.safe_load(ACTION.read_text(encoding="utf-8")) + return action["runs"]["steps"] + + +def test_pip_key_is_written_under_the_workspace() -> None: + writers = [s for s in _steps() if f"$GITHUB_WORKSPACE/{KEY_FILE}" in s.get("run", "")] + assert writers, "no step writes the pip key under $GITHUB_WORKSPACE" + + +def test_setup_python_hashes_the_workspace_key_file() -> None: + setup_python = [s for s in _steps() if str(s.get("uses", "")).startswith("actions/setup-python@")] + assert len(setup_python) == 1 + path = setup_python[0]["with"]["cache-dependency-path"] + assert path == f"${{{{ github.workspace }}}}/{KEY_FILE}", path + + +def test_no_step_keeps_the_key_under_runner_temp() -> None: + text = ACTION.read_text(encoding="utf-8") + assert "runner.temp }}/dokimi-setup" not in text + assert "$RUNNER_TEMP/dokimi-setup" not in text From 71dbbbf1abde620ddf0c920b6eae543234e1687d Mon Sep 17 00:00:00 2001 From: Nikolay Petrov Date: Sat, 31 Oct 2026 17:59:07 -0700 Subject: [PATCH 06/22] fix(setup): check the action out at dokimi-ref from its own repository The reusable workflow checked out the setup action from dokimi-repository, the package source, at job.workflow_sha. That expression is not a context property, so the ref was empty, and a fork's dokimi-repository asked the fork for a commit it may not hold. - New input dokimi-action-repository (default intel/dokimi) is the repository of the action checkout; all three checkouts use it. - The checkout ref is dokimi-ref, the commit the caller already pins in uses:. - repo-eval-self.yml passes its own repository, so a fork's self-check still finds its commit. The setup action also drops the paid and max-usd inputs and the DOKIMI_RUNS_DIR and DOKIMI_MAX_USD exports. Nothing enforced the cap, and no writer reads DOKIMI_RUNS_DIR; out-dir is the directory the caller writes to. Signed-off-by: Nikolay Petrov --- .github/workflows/repo-eval-self.yml | 1 + .github/workflows/repo-eval.yml | 23 ++++--- actions/setup/action.yml | 53 ++------------- dokimi/docs/repo-evaluation-contract.md | 4 +- .../tests/test_repo_eval_action_checkout.py | 65 +++++++++++++++++++ dokimi/tests/test_setup_action.py | 15 +++++ 6 files changed, 106 insertions(+), 55 deletions(-) create mode 100644 dokimi/tests/test_repo_eval_action_checkout.py diff --git a/.github/workflows/repo-eval-self.yml b/.github/workflows/repo-eval-self.yml index 52b0f4b21..dcc2fe6f2 100644 --- a/.github/workflows/repo-eval-self.yml +++ b/.github/workflows/repo-eval-self.yml @@ -46,6 +46,7 @@ jobs: # The commit under test: the merge commit on a pull request. dokimi-ref: ${{ github.sha }} dokimi-repository: ${{ github.repository }} + dokimi-action-repository: ${{ github.repository }} manifest: .agent-eval/repo-eval.yaml tasks: ${{ inputs.tasks || 'changed' }} smoke-tasks: fix-degrade-pre review-pr-conventions diff --git a/.github/workflows/repo-eval.yml b/.github/workflows/repo-eval.yml index 34fa72626..4e4ec56d4 100644 --- a/.github/workflows/repo-eval.yml +++ b/.github/workflows/repo-eval.yml @@ -34,7 +34,14 @@ on: required: true type: string dokimi-repository: - description: Repository dokimi-ref is in (owner/name), for testing a fork + description: Repository the dokimi package is installed from (owner/name), for testing a fork + required: false + type: string + default: intel/dokimi + dokimi-action-repository: + description: >- + Repository the setup action is checked out from (owner/name). It holds the commit + `dokimi-ref` names, so a fork's `dokimi-ref` needs this set to the fork too. required: false type: string default: intel/dokimi @@ -117,13 +124,13 @@ jobs: with: persist-credentials: false fetch-depth: 0 - # The setup action, from this workflow's own commit (job.workflow_sha), sparse to actions/. + # The setup action, at dokimi-ref (the commit the caller pins in `uses:`), sparse to actions/. # Kept until the job ends: the composite's post steps read it from this path. - name: Check out the setup action uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: - repository: ${{ inputs.dokimi-repository }} - ref: ${{ job.workflow_sha }} + repository: ${{ inputs.dokimi-action-repository }} + ref: ${{ inputs.dokimi-ref }} path: .dokimi-action sparse-checkout: /actions/ sparse-checkout-cone-mode: false @@ -280,8 +287,8 @@ jobs: - name: Check out the setup action uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: - repository: ${{ inputs.dokimi-repository }} - ref: ${{ job.workflow_sha }} + repository: ${{ inputs.dokimi-action-repository }} + ref: ${{ inputs.dokimi-ref }} path: .dokimi-action sparse-checkout: /actions/ sparse-checkout-cone-mode: false @@ -352,8 +359,8 @@ jobs: - name: Check out the setup action uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: - repository: ${{ inputs.dokimi-repository }} - ref: ${{ job.workflow_sha }} + repository: ${{ inputs.dokimi-action-repository }} + ref: ${{ inputs.dokimi-ref }} path: .dokimi-action sparse-checkout: /actions/ sparse-checkout-cone-mode: false diff --git a/actions/setup/action.yml b/actions/setup/action.yml index d3316cbac..46ba4c7b9 100644 --- a/actions/setup/action.yml +++ b/actions/setup/action.yml @@ -5,8 +5,7 @@ name: dokimi setup description: >- Installs dokimi at one pinned commit (a SHA, or a release tag resolved to its commit; a branch is refused) with its locked dependencies, sets up Python, and - exports what a dokimi run expects on CI: CI=true, DOKIMI_RUNS_DIR and, with - `max-usd`, DOKIMI_MAX_USD. Needs no secret. Callers grant `contents: read` at most. + exports CI=true. Needs no secret. Callers grant `contents: read` at most. inputs: dokimi-ref: @@ -15,7 +14,7 @@ inputs: `uses:` line that pins this action; a branch is refused. required: true dokimi-repository: - description: Repository dokimi-ref is in (owner/name), for testing a fork + description: Repository dokimi-ref is in (owner/name), for installing a fork's package required: false default: intel/dokimi python-version: @@ -25,8 +24,8 @@ inputs: out-dir: description: >- Directory for the run output, created by this action; empty means - `$RUNNER_TEMP/dokimi-out`. Its layout is fixed: `runs/` is the runs root - (DOKIMI_RUNS_DIR). Upload the whole directory as an artifact. + `$RUNNER_TEMP/dokimi-out`. The caller writes its output under it, and uploads the + whole directory as an artifact. required: false default: '' cache: @@ -35,18 +34,6 @@ inputs: changed lock is a new commit and a new key. required: false default: 'false' - paid: - description: >- - `true` when a step of this job calls a model and spends money. Then `max-usd` is - required. Free steps leave it `false` and need no cap. - required: false - default: 'false' - max-usd: - description: >- - The cap in US dollars on the whole job, exported as DOKIMI_MAX_USD. Required when - `paid` is `true`; a value that is not a positive number fails the job. - required: false - default: '' outputs: sha: @@ -59,26 +46,6 @@ outputs: runs: using: composite steps: - - name: Check the inputs - shell: bash - env: - PAID: ${{ inputs.paid }} - MAX_USD: ${{ inputs.max-usd }} - run: | - case "$PAID" in - true|false) ;; - *) echo "::error title=paid::paid must be 'true' or 'false', not '$PAID'"; exit 1 ;; - esac - if [ -n "$MAX_USD" ]; then - if ! [[ "$MAX_USD" =~ ^[0-9]+(\.[0-9]+)?$ ]] || ! awk -v v="$MAX_USD" 'BEGIN { exit !(v > 0) }'; then - echo "::error title=max-usd::max-usd must be a positive number of US dollars, not '$MAX_USD'" - exit 1 - fi - elif [ "$PAID" = true ]; then - echo "::error title=max-usd::max-usd is required when paid is true: a step here calls a model, and a run without a dollar cap is refused" - exit 1 - fi - # Resolves the ref before anything is installed: a tag becomes the commit it names # (the peeled line of ls-remote), so the jobs of one run install the same commit. - name: Resolve dokimi-ref @@ -138,17 +105,11 @@ runs: shell: bash env: OUT: ${{ inputs.out-dir }} - MAX_USD: ${{ inputs.max-usd }} - run: | # zizmor: ignore[github-env] -- intended: the two values written are checked above (no newline, a number) + run: | # zizmor: ignore[github-env] -- intended: the only value written to GITHUB_ENV is the constant CI=true out=${OUT:-$RUNNER_TEMP/dokimi-out} - mkdir -p "$out/runs" + mkdir -p "$out" dir=$(cd "$out" && pwd) - # Each value written to GITHUB_ENV is checked here too: a newline would add a variable. + # The path written to GITHUB_OUTPUT is checked here: a newline would add an output. if [[ "$dir" == *$'\n'* ]]; then echo "::error title=out-dir::out-dir has a newline"; exit 1; fi - if [ -n "$MAX_USD" ] && ! [[ "$MAX_USD" =~ ^[0-9]+(\.[0-9]+)?$ ]]; then - echo "::error title=max-usd::max-usd must be a positive number of US dollars"; exit 1 - fi echo "dir=$dir" >> "$GITHUB_OUTPUT" echo "CI=true" >> "$GITHUB_ENV" - echo "DOKIMI_RUNS_DIR=$dir/runs" >> "$GITHUB_ENV" - if [ -n "$MAX_USD" ]; then echo "DOKIMI_MAX_USD=$MAX_USD" >> "$GITHUB_ENV"; fi diff --git a/dokimi/docs/repo-evaluation-contract.md b/dokimi/docs/repo-evaluation-contract.md index dae1183cc..fa2a1b5c6 100644 --- a/dokimi/docs/repo-evaluation-contract.md +++ b/dokimi/docs/repo-evaluation-contract.md @@ -1493,7 +1493,9 @@ that runs the no-LLM checks on the caller's checkout and needs no secret. workflow, so it runs the smoke set. - Other inputs: `manifest`, `max-parallel` (4), `warm` (`--warm`, for a cache that starts empty on every runner), `runs-on` (`ubuntu-latest`), `timeout-minutes` - (180, per task) and `dokimi-repository` (`intel/dokimi`). + (180, per task), `dokimi-repository` (`intel/dokimi`, the package installed) and + `dokimi-action-repository` (`intel/dokimi`, where the setup action is checked out, at + `dokimi-ref`). Testing a fork's commit sets both to the fork. ```yaml # .github/workflows/agent-evals.yml in the repository diff --git a/dokimi/tests/test_repo_eval_action_checkout.py b/dokimi/tests/test_repo_eval_action_checkout.py new file mode 100644 index 000000000..60ae02533 --- /dev/null +++ b/dokimi/tests/test_repo_eval_action_checkout.py @@ -0,0 +1,65 @@ +# Copyright (C) 2026 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 + +"""repo-eval.yml: the setup action is checked out from its own repository, at dokimi-ref. + +Every job that uses the setup action checks it out first, into `.dokimi-action`. The checkout +takes its repository from `dokimi-action-repository` (default intel/dokimi), not from the +`dokimi-repository` that selects the package, and its ref from `dokimi-ref`, the commit the +caller pins in `uses:`. `job.workflow_sha` is not a context property, so it evaluated to +an empty ref. The self-check in repo-eval-self.yml passes its own repository, so a fork +still checks out the commit it runs. +""" + +from __future__ import annotations + +from pathlib import Path + +import yaml + +ROOT = Path(__file__).resolve().parents[2] +REPO_EVAL = ROOT / ".github/workflows/repo-eval.yml" +REPO_EVAL_SELF = ROOT / ".github/workflows/repo-eval-self.yml" +CHECKOUT_PATH = ".dokimi-action" + + +def _load(path: Path) -> dict: + return yaml.safe_load(path.read_text(encoding="utf-8")) + + +def _action_checkouts(workflow: dict) -> list[tuple[str, dict]]: + found = [] + for job_id, job in workflow["jobs"].items(): + for step in job.get("steps", []): + if str(step.get("uses", "")).startswith("actions/checkout@") and step.get("with", {}).get("path") == CHECKOUT_PATH: + found.append((job_id, step["with"])) + return found + + +def test_the_workflow_declares_the_action_repository_input_with_its_default() -> None: + inputs = _load(REPO_EVAL)[True]["workflow_call"]["inputs"] + assert "dokimi-action-repository" in inputs + assert inputs["dokimi-action-repository"]["default"] == "intel/dokimi" + + +def test_every_action_checkout_takes_its_repository_from_dokimi_action_repository() -> None: + checkouts = _action_checkouts(_load(REPO_EVAL)) + assert len(checkouts) == 3, checkouts + for job_id, with_ in checkouts: + assert with_["repository"] == "${{ inputs.dokimi-action-repository }}", job_id + + +def test_every_action_checkout_is_at_dokimi_ref() -> None: + checkouts = _action_checkouts(_load(REPO_EVAL)) + assert len(checkouts) == 3, checkouts + for job_id, with_ in checkouts: + assert with_["ref"] == "${{ inputs.dokimi-ref }}", job_id + + +def test_job_workflow_sha_is_not_used() -> None: + assert "job.workflow_sha" not in REPO_EVAL.read_text(encoding="utf-8") + + +def test_the_self_check_passes_its_own_repository_as_the_action_repository() -> None: + caller = _load(REPO_EVAL_SELF)["jobs"]["self"]["with"] + assert caller["dokimi-action-repository"] == "${{ github.repository }}" diff --git a/dokimi/tests/test_setup_action.py b/dokimi/tests/test_setup_action.py index 8015d4a31..467e09c77 100644 --- a/dokimi/tests/test_setup_action.py +++ b/dokimi/tests/test_setup_action.py @@ -41,3 +41,18 @@ def test_no_step_keeps_the_key_under_runner_temp() -> None: text = ACTION.read_text(encoding="utf-8") assert "runner.temp }}/dokimi-setup" not in text assert "$RUNNER_TEMP/dokimi-setup" not in text + + +def test_the_action_declares_no_paid_or_max_usd_input() -> None: + # A spending cap the action does not enforce must not ship as a safety input. + inputs = yaml.safe_load(ACTION.read_text(encoding="utf-8"))["inputs"] + assert "paid" not in inputs + assert "max-usd" not in inputs + + +def test_the_action_exports_no_dokimi_runs_dir_or_max_usd() -> None: + # DOKIMI_RUNS_DIR is read only by `dokimi runs`; the writers ignore it, so exporting it + # misleads. DOKIMI_MAX_USD is read by no code. Neither is exported or documented here. + text = ACTION.read_text(encoding="utf-8") + assert "DOKIMI_RUNS_DIR" not in text + assert "DOKIMI_MAX_USD" not in text From 5b88af05d6dadb835c1016b6a617955bf5840f04 Mon Sep 17 00:00:00 2001 From: Nikolay Petrov Date: Sat, 31 Oct 2026 18:07:34 -0700 Subject: [PATCH 07/22] fix(setup): drop a comment about a post step, widen zizmor's hook, tighten the key test - repo-eval.yml: the checkout comment said the composite's post steps read the checkout; the composite has no post step. - .pre-commit-config.yaml: the zizmor hook matched only actions//action.yml, not nested composites that the CI zizmor run on `actions` covers. - test_setup_action.py: the pip-key test matched the path anywhere in a step; it now needs the path to be the target of a redirect. Signed-off-by: Nikolay Petrov --- .github/workflows/repo-eval.yml | 1 - .pre-commit-config.yaml | 2 +- dokimi/tests/test_setup_action.py | 5 ++++- 3 files changed, 5 insertions(+), 3 deletions(-) diff --git a/.github/workflows/repo-eval.yml b/.github/workflows/repo-eval.yml index 4e4ec56d4..023d1d1fa 100644 --- a/.github/workflows/repo-eval.yml +++ b/.github/workflows/repo-eval.yml @@ -125,7 +125,6 @@ jobs: persist-credentials: false fetch-depth: 0 # The setup action, at dokimi-ref (the commit the caller pins in `uses:`), sparse to actions/. - # Kept until the job ends: the composite's post steps read it from this path. - name: Check out the setup action uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index f0df46567..a218fa776 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -48,7 +48,7 @@ repos: # The static job's zizmor step: the same command and scope (workflows and the composite actions). - id: zizmor args: [--no-progress, --offline, --min-severity, medium] - files: ^(\.github/workflows/[^/]+|actions/[^/]+/action)\.ya?ml$ + files: ^(\.github/workflows/[^/]+|actions/(.+/)?action)\.ya?ml$ - repo: https://github.com/gitleaks/gitleaks rev: 83d9cd684c87d95d656c1458ef04895a7f1cbd8e # frozen: v8.30.1 diff --git a/dokimi/tests/test_setup_action.py b/dokimi/tests/test_setup_action.py index 467e09c77..44f613011 100644 --- a/dokimi/tests/test_setup_action.py +++ b/dokimi/tests/test_setup_action.py @@ -12,6 +12,7 @@ from __future__ import annotations +import re from pathlib import Path import yaml @@ -26,7 +27,9 @@ def _steps() -> list[dict]: def test_pip_key_is_written_under_the_workspace() -> None: - writers = [s for s in _steps() if f"$GITHUB_WORKSPACE/{KEY_FILE}" in s.get("run", "")] + # The key must be the target of a redirect in a step: a path in a comment or an echo does not write it. + redirect = re.compile(r'>\s*"?\$GITHUB_WORKSPACE/' + re.escape(KEY_FILE)) + writers = [s for s in _steps() if redirect.search(s.get("run", ""))] assert writers, "no step writes the pip key under $GITHUB_WORKSPACE" From 3aa37ff8cd1471b38a355b9033d3bac414b56784 Mon Sep 17 00:00:00 2001 From: Nikolay Petrov Date: Sat, 31 Oct 2026 22:27:15 -0700 Subject: [PATCH 08/22] fix(setup): drop the pip cache; out-dir newline; pin the action commit CodeQL reports actions/cache-poisoning/poisonable-step on the setup steps: a pip cache restored in a run that checks out the caller's dokimi-ref is the path it flags. The cache input was opt-in and no caller or doc uses it, so setup-python runs without a cache, and the key file that fed it is gone. out-dir: a newline is refused before mkdir, and the captured absolute path keeps a trailing newline so the check after the capture sees it. validate records the commit the setup action was checked out at (action-sha). scan and tasks-check check the action out at that SHA, not at dokimi-ref again, so a tag that moves mid-run does not split the jobs. Signed-off-by: Nikolay Petrov --- .github/workflows/repo-eval.yml | 11 +++++- actions/setup/action.yml | 25 +++--------- .../tests/test_repo_eval_action_checkout.py | 19 ++++++--- dokimi/tests/test_setup_action.py | 39 ++++++++----------- 4 files changed, 46 insertions(+), 48 deletions(-) diff --git a/.github/workflows/repo-eval.yml b/.github/workflows/repo-eval.yml index 023d1d1fa..d62d7e39b 100644 --- a/.github/workflows/repo-eval.yml +++ b/.github/workflows/repo-eval.yml @@ -115,6 +115,7 @@ jobs: contents: read outputs: dokimi-sha: ${{ steps.dokimi.outputs.sha }} + action-sha: ${{ steps.action.outputs.sha }} id: ${{ steps.select.outputs.id }} tasks: ${{ steps.select.outputs.tasks }} steps: @@ -134,6 +135,12 @@ jobs: sparse-checkout: /actions/ sparse-checkout-cone-mode: false persist-credentials: false + # The commit the setup action was checked out at. The downstream jobs check the action out at + # this SHA: a tag in dokimi-ref may move after this job, and the package SHA does not name the action. + - name: Record the setup action commit + id: action + shell: bash + run: echo "sha=$(git -C .dokimi-action rev-parse HEAD)" >> "$GITHUB_OUTPUT" # Resolves dokimi-ref to a commit (the validate outputs pass it on) and installs dokimi. - name: Set up dokimi id: dokimi @@ -287,7 +294,7 @@ jobs: uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: repository: ${{ inputs.dokimi-action-repository }} - ref: ${{ inputs.dokimi-ref }} + ref: ${{ needs.validate.outputs.action-sha }} path: .dokimi-action sparse-checkout: /actions/ sparse-checkout-cone-mode: false @@ -359,7 +366,7 @@ jobs: uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: repository: ${{ inputs.dokimi-action-repository }} - ref: ${{ inputs.dokimi-ref }} + ref: ${{ needs.validate.outputs.action-sha }} path: .dokimi-action sparse-checkout: /actions/ sparse-checkout-cone-mode: false diff --git a/actions/setup/action.yml b/actions/setup/action.yml index 46ba4c7b9..a00f8eb53 100644 --- a/actions/setup/action.yml +++ b/actions/setup/action.yml @@ -28,12 +28,6 @@ inputs: whole directory as an artifact. required: false default: '' - cache: - description: >- - `true` restores and saves pip's download cache. The key is the resolved commit, so a - changed lock is a new commit and a new key. - required: false - default: 'false' outputs: sha: @@ -73,22 +67,12 @@ runs: echo "sha=$sha" >> "$GITHUB_OUTPUT" echo "dokimi $DOKIMI_REPO@$REF -> $sha" - # The pip cache key is a file that names the commit: setup-python hashes it. It sits in - # the workspace, since setup-python's hashFiles only sees files under GITHUB_WORKSPACE. - - name: Key for the pip cache - shell: bash - env: - SHA: ${{ steps.ref.outputs.sha }} - run: | - mkdir -p "$GITHUB_WORKSPACE/.dokimi-setup" - echo "dokimi $SHA" > "$GITHUB_WORKSPACE/.dokimi-setup/pip-key.txt" - + # No pip cache: a cache restored in a run that checks out the caller's dokimi-ref + # is the poisoning path CodeQL flags (actions/cache-poisoning/poisonable-step). - name: Set up Python uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0 with: python-version: ${{ inputs.python-version }} - cache: ${{ inputs.cache == 'true' && 'pip' || '' }} - cache-dependency-path: ${{ github.workspace }}/.dokimi-setup/pip-key.txt - name: Install dokimi shell: bash @@ -107,8 +91,11 @@ runs: OUT: ${{ inputs.out-dir }} run: | # zizmor: ignore[github-env] -- intended: the only value written to GITHUB_ENV is the constant CI=true out=${OUT:-$RUNNER_TEMP/dokimi-out} + # Refused before mkdir, so a newline name is never created. + if [[ "$out" == *$'\n'* ]]; then echo "::error title=out-dir::out-dir has a newline"; exit 1; fi mkdir -p "$out" - dir=$(cd "$out" && pwd) + # The x keeps a trailing newline from being stripped by the command substitution. + dir=$(cd "$out" && pwd && printf x); dir=${dir%x} # The path written to GITHUB_OUTPUT is checked here: a newline would add an output. if [[ "$dir" == *$'\n'* ]]; then echo "::error title=out-dir::out-dir has a newline"; exit 1; fi echo "dir=$dir" >> "$GITHUB_OUTPUT" diff --git a/dokimi/tests/test_repo_eval_action_checkout.py b/dokimi/tests/test_repo_eval_action_checkout.py index 60ae02533..2a347c2b9 100644 --- a/dokimi/tests/test_repo_eval_action_checkout.py +++ b/dokimi/tests/test_repo_eval_action_checkout.py @@ -49,11 +49,20 @@ def test_every_action_checkout_takes_its_repository_from_dokimi_action_repositor assert with_["repository"] == "${{ inputs.dokimi-action-repository }}", job_id -def test_every_action_checkout_is_at_dokimi_ref() -> None: - checkouts = _action_checkouts(_load(REPO_EVAL)) - assert len(checkouts) == 3, checkouts - for job_id, with_ in checkouts: - assert with_["ref"] == "${{ inputs.dokimi-ref }}", job_id +def test_validate_checks_the_action_out_at_dokimi_ref_and_records_its_commit() -> None: + workflow = _load(REPO_EVAL) + checkouts = dict(_action_checkouts(workflow)) + assert checkouts["validate"]["ref"] == "${{ inputs.dokimi-ref }}" + assert "action-sha" in workflow["jobs"]["validate"]["outputs"] + recorder = [s for s in workflow["jobs"]["validate"]["steps"] if s.get("id") == "action"] + assert recorder and "rev-parse HEAD" in recorder[0]["run"] + + +def test_downstream_jobs_check_the_action_out_at_the_commit_validate_recorded() -> None: + # A tag in dokimi-ref may move while validate runs; the package SHA does not name the action. + checkouts = dict(_action_checkouts(_load(REPO_EVAL))) + for job_id in ("scan", "tasks-check"): + assert checkouts[job_id]["ref"] == "${{ needs.validate.outputs.action-sha }}", job_id def test_job_workflow_sha_is_not_used() -> None: diff --git a/dokimi/tests/test_setup_action.py b/dokimi/tests/test_setup_action.py index 44f613011..02c71d940 100644 --- a/dokimi/tests/test_setup_action.py +++ b/dokimi/tests/test_setup_action.py @@ -1,24 +1,21 @@ # Copyright (C) 2026 Intel Corporation # SPDX-License-Identifier: Apache-2.0 -"""actions/setup/action.yml: the pip cache key must sit where setup-python can hash it. +"""actions/setup/action.yml: no pip cache, and an out-dir newline is refused before it is created. -setup-python hashes ``cache-dependency-path`` with ``@actions/glob.hashFiles``, which only -sees files under GITHUB_WORKSPACE. A key file in ``runner.temp`` hashes to nothing, and -setup-python then fails with no dependency file matched. The check reads the composite's -steps and pins that the key is written under the workspace and that setup-python is given -the same path. +A pip cache restored in a run that checks out the caller's dokimi-ref is the cache-poisoning path +CodeQL reports (actions/cache-poisoning/poisonable-step), so setup-python runs without a cache. +A newline in out-dir is refused before mkdir, and the captured path keeps a trailing newline so the +check after the capture sees it. """ from __future__ import annotations -import re from pathlib import Path import yaml ACTION = Path(__file__).resolve().parents[2] / "actions/setup/action.yml" -KEY_FILE = ".dokimi-setup/pip-key.txt" def _steps() -> list[dict]: @@ -26,24 +23,22 @@ def _steps() -> list[dict]: return action["runs"]["steps"] -def test_pip_key_is_written_under_the_workspace() -> None: - # The key must be the target of a redirect in a step: a path in a comment or an echo does not write it. - redirect = re.compile(r'>\s*"?\$GITHUB_WORKSPACE/' + re.escape(KEY_FILE)) - writers = [s for s in _steps() if redirect.search(s.get("run", ""))] - assert writers, "no step writes the pip key under $GITHUB_WORKSPACE" - - -def test_setup_python_hashes_the_workspace_key_file() -> None: +def test_setup_python_runs_without_a_cache() -> None: setup_python = [s for s in _steps() if str(s.get("uses", "")).startswith("actions/setup-python@")] assert len(setup_python) == 1 - path = setup_python[0]["with"]["cache-dependency-path"] - assert path == f"${{{{ github.workspace }}}}/{KEY_FILE}", path + assert "cache" not in setup_python[0].get("with", {}) + assert "cache" not in yaml.safe_load(ACTION.read_text(encoding="utf-8"))["inputs"] -def test_no_step_keeps_the_key_under_runner_temp() -> None: - text = ACTION.read_text(encoding="utf-8") - assert "runner.temp }}/dokimi-setup" not in text - assert "$RUNNER_TEMP/dokimi-setup" not in text +def test_out_dir_newline_is_refused_before_mkdir() -> None: + run = next(s["run"] for s in _steps() if s.get("id") == "out") + refuse = run.index("*$'\\n'*") + assert refuse < run.index('mkdir -p "$out"') + + +def test_out_dir_capture_keeps_a_trailing_newline() -> None: + run = next(s["run"] for s in _steps() if s.get("id") == "out") + assert "printf x" in run and "${dir%x}" in run def test_the_action_declares_no_paid_or_max_usd_input() -> None: From 16df6e9bcf90e8e91f262e52d3f46efacf29d936 Mon Sep 17 00:00:00 2001 From: Nikolay Petrov Date: Sat, 31 Oct 2026 23:36:08 -0700 Subject: [PATCH 09/22] fix(setup): keep the out-dir path intact when capturing it pwd prints a line terminator, so the capture always kept a newline and the next check refused every out-dir, failing validate and its dependents. The capture keeps the sentinel so a newline at the end of the path is still seen and refused. The tests now run the step script with bash, covering the default path, an out-dir with a newline, and a cwd ending in a newline. Signed-off-by: Nikolay Petrov --- actions/setup/action.yml | 5 ++- dokimi/tests/test_setup_action.py | 58 +++++++++++++++++++++++++------ 2 files changed, 49 insertions(+), 14 deletions(-) diff --git a/actions/setup/action.yml b/actions/setup/action.yml index a00f8eb53..d620e7593 100644 --- a/actions/setup/action.yml +++ b/actions/setup/action.yml @@ -94,9 +94,8 @@ runs: # Refused before mkdir, so a newline name is never created. if [[ "$out" == *$'\n'* ]]; then echo "::error title=out-dir::out-dir has a newline"; exit 1; fi mkdir -p "$out" - # The x keeps a trailing newline from being stripped by the command substitution. - dir=$(cd "$out" && pwd && printf x); dir=${dir%x} - # The path written to GITHUB_OUTPUT is checked here: a newline would add an output. + # The x keeps a newline at the end of $PWD from being stripped by the command substitution. + dir=$(cd "$out" && printf '%sx' "$PWD"); dir=${dir%x} if [[ "$dir" == *$'\n'* ]]; then echo "::error title=out-dir::out-dir has a newline"; exit 1; fi echo "dir=$dir" >> "$GITHUB_OUTPUT" echo "CI=true" >> "$GITHUB_ENV" diff --git a/dokimi/tests/test_setup_action.py b/dokimi/tests/test_setup_action.py index 02c71d940..f678ed040 100644 --- a/dokimi/tests/test_setup_action.py +++ b/dokimi/tests/test_setup_action.py @@ -5,12 +5,13 @@ A pip cache restored in a run that checks out the caller's dokimi-ref is the cache-poisoning path CodeQL reports (actions/cache-poisoning/poisonable-step), so setup-python runs without a cache. -A newline in out-dir is refused before mkdir, and the captured path keeps a trailing newline so the -check after the capture sees it. +A newline in out-dir is refused before mkdir; the captured path is written without a newline. """ from __future__ import annotations +import os +import subprocess from pathlib import Path import yaml @@ -30,15 +31,50 @@ def test_setup_python_runs_without_a_cache() -> None: assert "cache" not in yaml.safe_load(ACTION.read_text(encoding="utf-8"))["inputs"] -def test_out_dir_newline_is_refused_before_mkdir() -> None: - run = next(s["run"] for s in _steps() if s.get("id") == "out") - refuse = run.index("*$'\\n'*") - assert refuse < run.index('mkdir -p "$out"') - - -def test_out_dir_capture_keeps_a_trailing_newline() -> None: - run = next(s["run"] for s in _steps() if s.get("id") == "out") - assert "printf x" in run and "${dir%x}" in run +def _run_out_step(tmp_path: Path, out_dir: str, cwd: Path | None = None) -> subprocess.CompletedProcess[str]: + # Runs the output step's script the way the runner does: bash -e -o pipefail, with the + # runner's env files and RUNNER_TEMP pointed at tmp_path. + script = next(s["run"] for s in _steps() if s.get("id") == "out") + env = { + "PATH": os.environ["PATH"], + "RUNNER_TEMP": str(tmp_path / "runner-temp"), + "GITHUB_OUTPUT": str(tmp_path / "output"), + "GITHUB_ENV": str(tmp_path / "env"), + "OUT": out_dir, + } + (tmp_path / "runner-temp").mkdir() + for name in ("output", "env"): + (tmp_path / name).touch() + return subprocess.run( + ["bash", "--noprofile", "--norc", "-e", "-o", "pipefail", "-c", script], + env=env, capture_output=True, text=True, check=False, cwd=cwd or tmp_path, + ) + + +def test_out_dir_default_is_written_as_a_clean_path(tmp_path: Path) -> None: + result = _run_out_step(tmp_path, "") + assert result.returncode == 0, result.stderr + output = (tmp_path / "output").read_text(encoding="utf-8") + expected = (tmp_path / "runner-temp" / "dokimi-out").resolve() + assert output == f"dir={expected}\n" + + +def test_a_cwd_ending_in_a_newline_is_refused(tmp_path: Path) -> None: + # The path that reaches GITHUB_OUTPUT must not lose a newline at its end. + cwd = tmp_path / "work\n" + cwd.mkdir() + result = _run_out_step(tmp_path, "out", cwd=cwd) + assert result.returncode != 0 + assert "out-dir has a newline" in result.stdout + assert (tmp_path / "output").read_text(encoding="utf-8") == "" + + +def test_out_dir_with_a_newline_is_refused_before_mkdir(tmp_path: Path) -> None: + result = _run_out_step(tmp_path, "reports\n") + assert result.returncode != 0 + assert "out-dir has a newline" in result.stdout + assert not any(p.name.startswith("reports") for p in tmp_path.iterdir()) + assert (tmp_path / "output").read_text(encoding="utf-8") == "" def test_the_action_declares_no_paid_or_max_usd_input() -> None: From ff84af50db6cf1542c239d0c0984359bfe86749b Mon Sep 17 00:00:00 2001 From: Anton Volkov Date: Sun, 1 Nov 2026 14:25:04 +0000 Subject: [PATCH 10/22] Treat an out-dir starting with a dash as a path Signed-off-by: Anton Volkov --- actions/setup/action.yml | 4 ++-- dokimi/tests/test_setup_action.py | 7 +++++++ 2 files changed, 9 insertions(+), 2 deletions(-) diff --git a/actions/setup/action.yml b/actions/setup/action.yml index d620e7593..bc7e8a1d8 100644 --- a/actions/setup/action.yml +++ b/actions/setup/action.yml @@ -93,9 +93,9 @@ runs: out=${OUT:-$RUNNER_TEMP/dokimi-out} # Refused before mkdir, so a newline name is never created. if [[ "$out" == *$'\n'* ]]; then echo "::error title=out-dir::out-dir has a newline"; exit 1; fi - mkdir -p "$out" + mkdir -p -- "$out" # The x keeps a newline at the end of $PWD from being stripped by the command substitution. - dir=$(cd "$out" && printf '%sx' "$PWD"); dir=${dir%x} + dir=$(cd -- "$out" && printf '%sx' "$PWD"); dir=${dir%x} if [[ "$dir" == *$'\n'* ]]; then echo "::error title=out-dir::out-dir has a newline"; exit 1; fi echo "dir=$dir" >> "$GITHUB_OUTPUT" echo "CI=true" >> "$GITHUB_ENV" diff --git a/dokimi/tests/test_setup_action.py b/dokimi/tests/test_setup_action.py index f678ed040..2f1177bfd 100644 --- a/dokimi/tests/test_setup_action.py +++ b/dokimi/tests/test_setup_action.py @@ -59,6 +59,13 @@ def test_out_dir_default_is_written_as_a_clean_path(tmp_path: Path) -> None: assert output == f"dir={expected}\n" +def test_out_dir_starting_with_a_dash_is_a_path_not_an_option(tmp_path: Path) -> None: + result = _run_out_step(tmp_path, "-reports") + assert result.returncode == 0, result.stderr + expected = (tmp_path / "-reports").resolve() + assert (tmp_path / "output").read_text(encoding="utf-8") == f"dir={expected}\n" + + def test_a_cwd_ending_in_a_newline_is_refused(tmp_path: Path) -> None: # The path that reaches GITHUB_OUTPUT must not lose a newline at its end. cwd = tmp_path / "work\n" From 3181a5101530af8ab17da11f3a44c310c6d072bb Mon Sep 17 00:00:00 2001 From: Anton Volkov Date: Sun, 1 Nov 2026 14:25:10 +0000 Subject: [PATCH 11/22] Run the newline cwd test with out-dir set to a dot Signed-off-by: Anton Volkov --- dokimi/tests/test_setup_action.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dokimi/tests/test_setup_action.py b/dokimi/tests/test_setup_action.py index 2f1177bfd..556b752f7 100644 --- a/dokimi/tests/test_setup_action.py +++ b/dokimi/tests/test_setup_action.py @@ -70,7 +70,7 @@ def test_a_cwd_ending_in_a_newline_is_refused(tmp_path: Path) -> None: # The path that reaches GITHUB_OUTPUT must not lose a newline at its end. cwd = tmp_path / "work\n" cwd.mkdir() - result = _run_out_step(tmp_path, "out", cwd=cwd) + result = _run_out_step(tmp_path, ".", cwd=cwd) assert result.returncode != 0 assert "out-dir has a newline" in result.stdout assert (tmp_path / "output").read_text(encoding="utf-8") == "" From a2f5d4650efca3309918ec89b8d2150ff22b4a27 Mon Sep 17 00:00:00 2001 From: Anton Volkov Date: Sun, 1 Nov 2026 14:25:15 +0000 Subject: [PATCH 12/22] Assert the exact action-sha mapping and recorder command Signed-off-by: Anton Volkov --- dokimi/tests/test_repo_eval_action_checkout.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/dokimi/tests/test_repo_eval_action_checkout.py b/dokimi/tests/test_repo_eval_action_checkout.py index 2a347c2b9..cb4b7ae15 100644 --- a/dokimi/tests/test_repo_eval_action_checkout.py +++ b/dokimi/tests/test_repo_eval_action_checkout.py @@ -53,9 +53,9 @@ def test_validate_checks_the_action_out_at_dokimi_ref_and_records_its_commit() - workflow = _load(REPO_EVAL) checkouts = dict(_action_checkouts(workflow)) assert checkouts["validate"]["ref"] == "${{ inputs.dokimi-ref }}" - assert "action-sha" in workflow["jobs"]["validate"]["outputs"] + assert workflow["jobs"]["validate"]["outputs"]["action-sha"] == "${{ steps.action.outputs.sha }}" recorder = [s for s in workflow["jobs"]["validate"]["steps"] if s.get("id") == "action"] - assert recorder and "rev-parse HEAD" in recorder[0]["run"] + assert recorder and recorder[0]["run"] == 'echo "sha=$(git -C .dokimi-action rev-parse HEAD)" >> "$GITHUB_OUTPUT"' def test_downstream_jobs_check_the_action_out_at_the_commit_validate_recorded() -> None: From 08808992feae3610761b7e154d0f68e03678d2ad Mon Sep 17 00:00:00 2001 From: Anton Volkov Date: Sun, 1 Nov 2026 14:59:12 +0000 Subject: [PATCH 13/22] Prefix a relative out-dir with ./ so a lone dash is a path Signed-off-by: Anton Volkov --- actions/setup/action.yml | 2 ++ dokimi/tests/test_setup_action.py | 19 +++++++++++++++++-- 2 files changed, 19 insertions(+), 2 deletions(-) diff --git a/actions/setup/action.yml b/actions/setup/action.yml index bc7e8a1d8..e48bf946d 100644 --- a/actions/setup/action.yml +++ b/actions/setup/action.yml @@ -93,6 +93,8 @@ runs: out=${OUT:-$RUNNER_TEMP/dokimi-out} # Refused before mkdir, so a newline name is never created. if [[ "$out" == *$'\n'* ]]; then echo "::error title=out-dir::out-dir has a newline"; exit 1; fi + # A relative path gets ./ so that a lone - is the directory, not cd's OLDPWD shorthand. + [[ "$out" == /* ]] || out=./$out mkdir -p -- "$out" # The x keeps a newline at the end of $PWD from being stripped by the command substitution. dir=$(cd -- "$out" && printf '%sx' "$PWD"); dir=${dir%x} diff --git a/dokimi/tests/test_setup_action.py b/dokimi/tests/test_setup_action.py index 556b752f7..29f972ef2 100644 --- a/dokimi/tests/test_setup_action.py +++ b/dokimi/tests/test_setup_action.py @@ -31,7 +31,7 @@ def test_setup_python_runs_without_a_cache() -> None: assert "cache" not in yaml.safe_load(ACTION.read_text(encoding="utf-8"))["inputs"] -def _run_out_step(tmp_path: Path, out_dir: str, cwd: Path | None = None) -> subprocess.CompletedProcess[str]: +def _run_out_step(tmp_path: Path, out_dir: str, cwd: Path | None = None, prelude: str = "") -> subprocess.CompletedProcess[str]: # Runs the output step's script the way the runner does: bash -e -o pipefail, with the # runner's env files and RUNNER_TEMP pointed at tmp_path. script = next(s["run"] for s in _steps() if s.get("id") == "out") @@ -46,7 +46,7 @@ def _run_out_step(tmp_path: Path, out_dir: str, cwd: Path | None = None) -> subp for name in ("output", "env"): (tmp_path / name).touch() return subprocess.run( - ["bash", "--noprofile", "--norc", "-e", "-o", "pipefail", "-c", script], + ["bash", "--noprofile", "--norc", "-e", "-o", "pipefail", "-c", prelude + script], env=env, capture_output=True, text=True, check=False, cwd=cwd or tmp_path, ) @@ -66,6 +66,21 @@ def test_out_dir_starting_with_a_dash_is_a_path_not_an_option(tmp_path: Path) -> assert (tmp_path / "output").read_text(encoding="utf-8") == f"dir={expected}\n" +def test_out_dir_of_a_lone_dash_is_the_directory_not_oldpwd(tmp_path: Path) -> None: + result = _run_out_step(tmp_path, "-") + assert result.returncode == 0, result.stderr + assert (tmp_path / "output").read_text(encoding="utf-8") == f"dir={(tmp_path / '-').resolve()}\n" + + +def test_out_dir_of_a_lone_dash_with_oldpwd_set(tmp_path: Path) -> None: + # OLDPWD is set by a leading cd in the script, as it would be after any earlier cd. + script_cwd = tmp_path / "work" + script_cwd.mkdir() + result = _run_out_step(tmp_path, "-", cwd=script_cwd, prelude="cd /tmp\ncd -- \"$OLDPWD\"\n") + assert result.returncode == 0, result.stderr + assert (tmp_path / "output").read_text(encoding="utf-8") == f"dir={(script_cwd / '-').resolve()}\n" + + def test_a_cwd_ending_in_a_newline_is_refused(tmp_path: Path) -> None: # The path that reaches GITHUB_OUTPUT must not lose a newline at its end. cwd = tmp_path / "work\n" From fe4f24734b3a267821dbde53b7879f019f5345ec Mon Sep 17 00:00:00 2001 From: Nikolay Petrov Date: Sun, 1 Nov 2026 09:54:02 -0800 Subject: [PATCH 14/22] ci(repo-eval): check the setup action out at a commit resolved first dokimi-ref used to go to actions/checkout as is, so a branch named like a tag could make the checkout run branch code. The validate job now resolves the ref to a commit before the checkout: a 40-digit SHA passes through, a tag resolves to its peeled commit, anything else is refused. The test runs the resolver with a fake git that lists a tag and its peeled commit. Signed-off-by: Nikolay Petrov --- .github/workflows/repo-eval.yml | 32 ++++++++++++- .../tests/test_repo_eval_action_checkout.py | 46 ++++++++++++++++++- 2 files changed, 75 insertions(+), 3 deletions(-) diff --git a/.github/workflows/repo-eval.yml b/.github/workflows/repo-eval.yml index d62d7e39b..b2facd6cd 100644 --- a/.github/workflows/repo-eval.yml +++ b/.github/workflows/repo-eval.yml @@ -125,12 +125,40 @@ jobs: with: persist-credentials: false fetch-depth: 0 - # The setup action, at dokimi-ref (the commit the caller pins in `uses:`), sparse to actions/. + # The setup action's commit, resolved before anything is checked out. A tag and a branch can share + # a name, and actions/checkout takes the branch for a bare name, so the ref is passed as a commit: + # a 40-digit SHA as it is, a tag as the commit its peeled ls-remote line names. Anything else (a branch) + # is refused here, before any code of the action repository runs. + - name: Resolve the setup action commit + id: action-ref + shell: bash + env: + REF: ${{ inputs.dokimi-ref }} + ACTION_REPO: ${{ inputs.dokimi-action-repository }} + run: | + if ! [[ "$ACTION_REPO" =~ ^[A-Za-z0-9-]+/[A-Za-z0-9._-]+$ ]]; then + echo "::error title=dokimi-action-repository::dokimi-action-repository is not owner/name" + exit 1 + fi + sha= + if [[ "$REF" =~ ^[0-9a-f]{40}$ ]]; then + sha=$REF + elif [[ "$REF" =~ ^[A-Za-z0-9][A-Za-z0-9._-]*$ ]]; then + sha=$(git ls-remote --tags "https://github.com/$ACTION_REPO" \ + | awk -v t="refs/tags/$REF" '$2 == t "^{}" { p = $1 } $2 == t { l = $1 } END { print (p != "" ? p : l) }') + fi + if ! [[ "$sha" =~ ^[0-9a-f]{40}$ ]]; then + echo "::error title=dokimi-ref::dokimi-ref is neither a 40-digit commit SHA nor a tag of $ACTION_REPO" + exit 1 + fi + echo "sha=$sha" >> "$GITHUB_OUTPUT" + + # The setup action, at the commit resolved above (sparse to actions/). - name: Check out the setup action uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: repository: ${{ inputs.dokimi-action-repository }} - ref: ${{ inputs.dokimi-ref }} + ref: ${{ steps.action-ref.outputs.sha }} path: .dokimi-action sparse-checkout: /actions/ sparse-checkout-cone-mode: false diff --git a/dokimi/tests/test_repo_eval_action_checkout.py b/dokimi/tests/test_repo_eval_action_checkout.py index cb4b7ae15..b90fc38fe 100644 --- a/dokimi/tests/test_repo_eval_action_checkout.py +++ b/dokimi/tests/test_repo_eval_action_checkout.py @@ -52,7 +52,7 @@ def test_every_action_checkout_takes_its_repository_from_dokimi_action_repositor def test_validate_checks_the_action_out_at_dokimi_ref_and_records_its_commit() -> None: workflow = _load(REPO_EVAL) checkouts = dict(_action_checkouts(workflow)) - assert checkouts["validate"]["ref"] == "${{ inputs.dokimi-ref }}" + assert checkouts["validate"]["ref"] == "${{ steps.action-ref.outputs.sha }}" assert workflow["jobs"]["validate"]["outputs"]["action-sha"] == "${{ steps.action.outputs.sha }}" recorder = [s for s in workflow["jobs"]["validate"]["steps"] if s.get("id") == "action"] assert recorder and recorder[0]["run"] == 'echo "sha=$(git -C .dokimi-action rev-parse HEAD)" >> "$GITHUB_OUTPUT"' @@ -72,3 +72,47 @@ def test_job_workflow_sha_is_not_used() -> None: def test_the_self_check_passes_its_own_repository_as_the_action_repository() -> None: caller = _load(REPO_EVAL_SELF)["jobs"]["self"]["with"] assert caller["dokimi-action-repository"] == "${{ github.repository }}" + + +def _validate_steps() -> list[dict]: + return _load(REPO_EVAL)["jobs"]["validate"]["steps"] + + +def test_validate_checks_the_action_out_at_a_commit_resolved_before_the_checkout() -> None: + # A branch named like a tag would win a bare ref in actions/checkout: the commit is resolved first. + steps = _validate_steps() + resolve = next(s for s in steps if s.get("id") == "action-ref") + checkout = next(s for s in steps if s.get("name") == "Check out the setup action") + assert steps.index(resolve) < steps.index(checkout) + assert checkout["with"]["ref"] == "${{ steps.action-ref.outputs.sha }}" + assert "ls-remote --tags" in resolve["run"] and "^{}" in resolve["run"] + + +def test_the_setup_action_commit_resolver_takes_a_tag_as_its_peeled_commit(tmp_path: Path) -> None: + # Runs the step as the runner does, against a fake `git ls-remote` that lists a tag and its peeled commit. + import os + import subprocess + + resolve = next(s for s in _validate_steps() if s.get("id") == "action-ref") + bin_dir = tmp_path / "bin" + bin_dir.mkdir() + fake_git = bin_dir / "git" + fake_git.write_text("#!/bin/sh\n" + "printf '%s\\n' '1111111111111111111111111111111111111111\trefs/tags/release' " + "'2222222222222222222222222222222222222222\trefs/tags/release^{}'\n", encoding="utf-8") + fake_git.chmod(0o755) + script = tmp_path / "step.sh" + script.write_text(resolve["run"], encoding="utf-8") + + def resolve_ref(ref: str) -> str: + out = tmp_path / "output" + out.write_text("", encoding="utf-8") + env = {**os.environ, "PATH": f"{bin_dir}:{os.environ['PATH']}", "GITHUB_OUTPUT": str(out), + "REF": ref, "ACTION_REPO": "intel/dokimi"} + result = subprocess.run(["bash", "-e", "-o", "pipefail", str(script)], env=env, + capture_output=True, text=True) + assert result.returncode == 0, result.stderr + return out.read_text(encoding="utf-8") + + assert resolve_ref("release") == "sha=2222222222222222222222222222222222222222\n" + assert resolve_ref("3" * 40) == f"sha={'3' * 40}\n" From ced2d6646d7b2e428b23c7b507137ae74e3054ec Mon Sep 17 00:00:00 2001 From: Nikolay Petrov Date: Sun, 1 Nov 2026 11:14:45 -0800 Subject: [PATCH 15/22] ci(repo-eval): install the action's resolved commit when both repos match validate resolved dokimi-ref for the action checkout, then the setup step resolved the same tag again with its own ls-remote. A tag that moved in between made validate run one commit and install another. With dokimi-repository equal to dokimi-action-repository the setup step now gets the resolved commit; a different package repository still resolves its own ref. Tests: the wiring is pinned in test_repo_eval_action_checkout; a commit ref is installed as given with no ls-remote, and a tag resolves to its peeled commit. Comment block in repo-eval.yml cut to two lines (check_comment_blocks). Signed-off-by: Nikolay Petrov --- .github/workflows/repo-eval.yml | 11 +++-- .../tests/test_repo_eval_action_checkout.py | 10 +++++ dokimi/tests/test_setup_action.py | 43 +++++++++++++++++++ 3 files changed, 58 insertions(+), 6 deletions(-) diff --git a/.github/workflows/repo-eval.yml b/.github/workflows/repo-eval.yml index b2facd6cd..bad3e3822 100644 --- a/.github/workflows/repo-eval.yml +++ b/.github/workflows/repo-eval.yml @@ -125,10 +125,8 @@ jobs: with: persist-credentials: false fetch-depth: 0 - # The setup action's commit, resolved before anything is checked out. A tag and a branch can share - # a name, and actions/checkout takes the branch for a bare name, so the ref is passed as a commit: - # a 40-digit SHA as it is, a tag as the commit its peeled ls-remote line names. Anything else (a branch) - # is refused here, before any code of the action repository runs. + # The setup action's commit, resolved before any checkout: actions/checkout takes a branch that shares a + # tag's name, so the ref goes in as a commit. A branch is refused here, before the action's code runs. - name: Resolve the setup action commit id: action-ref shell: bash @@ -169,12 +167,13 @@ jobs: id: action shell: bash run: echo "sha=$(git -C .dokimi-action rev-parse HEAD)" >> "$GITHUB_OUTPUT" - # Resolves dokimi-ref to a commit (the validate outputs pass it on) and installs dokimi. + # Installs dokimi. With one repository for both, the commit resolved above is passed on: a second + # ls-remote of a tag that has moved would install another commit than the action it runs. - name: Set up dokimi id: dokimi uses: ./.dokimi-action/actions/setup with: - dokimi-ref: ${{ inputs.dokimi-ref }} + dokimi-ref: ${{ inputs.dokimi-repository == inputs.dokimi-action-repository && steps.action-ref.outputs.sha || inputs.dokimi-ref }} dokimi-repository: ${{ inputs.dokimi-repository }} python-version: '3.13' - name: repo validate diff --git a/dokimi/tests/test_repo_eval_action_checkout.py b/dokimi/tests/test_repo_eval_action_checkout.py index b90fc38fe..d2f020c29 100644 --- a/dokimi/tests/test_repo_eval_action_checkout.py +++ b/dokimi/tests/test_repo_eval_action_checkout.py @@ -116,3 +116,13 @@ def resolve_ref(ref: str) -> str: assert resolve_ref("release") == "sha=2222222222222222222222222222222222222222\n" assert resolve_ref("3" * 40) == f"sha={'3' * 40}\n" + + +def test_validate_installs_the_commit_it_resolved_when_the_repositories_match() -> None: + # Two ls-remote calls for one tag can name two commits if the tag moves between them. With one + # repository for the package and the action, the setup step gets the commit resolved above; a + # different package repository resolves its own ref, as before. + setup = next(s for s in _validate_steps() if s.get("id") == "dokimi") + assert setup["with"]["dokimi-ref"] == ( + "${{ inputs.dokimi-repository == inputs.dokimi-action-repository" + " && steps.action-ref.outputs.sha || inputs.dokimi-ref }}") diff --git a/dokimi/tests/test_setup_action.py b/dokimi/tests/test_setup_action.py index 29f972ef2..a6d25a1bc 100644 --- a/dokimi/tests/test_setup_action.py +++ b/dokimi/tests/test_setup_action.py @@ -99,6 +99,49 @@ def test_out_dir_with_a_newline_is_refused_before_mkdir(tmp_path: Path) -> None: assert (tmp_path / "output").read_text(encoding="utf-8") == "" +TAG_OBJECT = "a" * 40 # the annotated tag's own object +PEELED = "b" * 40 # the commit the tag names (its ^{} line) + + +def _resolve_ref(tmp_path: Path, ref: str) -> tuple[subprocess.CompletedProcess[str], str, str]: + # Runs the ref step the way the runner does, with a fake git that logs its calls and lists a tag. + # Returns (result, the git calls logged, the sha written to GITHUB_OUTPUT). + bin_dir = tmp_path / "bin" + bin_dir.mkdir() + log = tmp_path / "git.log" + fake = bin_dir / "git" + fake.write_text( + "#!/bin/sh\n" + f"echo \"$*\" >> '{log}'\n" + f"printf '%s\\trefs/tags/release\\n%s\\trefs/tags/release^{{}}\\n' '{TAG_OBJECT}' '{PEELED}'\n", + encoding="utf-8") + fake.chmod(0o755) + output = tmp_path / "output" + output.touch() + script = next(s["run"] for s in _steps() if s.get("id") == "ref") + env = {"PATH": f"{bin_dir}{os.pathsep}{os.environ['PATH']}", "REF": ref, + "DOKIMI_REPO": "intel/dokimi", "GITHUB_OUTPUT": str(output)} + result = subprocess.run(["bash", "--noprofile", "--norc", "-e", "-o", "pipefail", "-c", script], + env=env, capture_output=True, text=True, check=False, cwd=tmp_path) + calls = log.read_text(encoding="utf-8") if log.exists() else "" + return result, calls, output.read_text(encoding="utf-8") + + +def test_a_commit_ref_is_installed_as_given_and_the_tag_is_not_looked_up_again(tmp_path: Path) -> None: + # repo-eval passes the commit it resolved, so a tag that moved since cannot change what is installed. + result, calls, output = _resolve_ref(tmp_path, PEELED) + assert result.returncode == 0, result.stderr + assert calls == "" + assert output == f"sha={PEELED}\n" + + +def test_a_tag_ref_resolves_to_the_commit_it_names(tmp_path: Path) -> None: + result, calls, output = _resolve_ref(tmp_path, "release") + assert result.returncode == 0, result.stderr + assert "ls-remote --tags" in calls + assert output == f"sha={PEELED}\n" + + def test_the_action_declares_no_paid_or_max_usd_input() -> None: # A spending cap the action does not enforce must not ship as a safety input. inputs = yaml.safe_load(ACTION.read_text(encoding="utf-8"))["inputs"] From 1817460e77d2559f98f19da14fd65d9f55e36a9d Mon Sep 17 00:00:00 2001 From: Nikolay Petrov Date: Sun, 1 Nov 2026 11:14:50 -0800 Subject: [PATCH 16/22] test(ci): two-line comment for the repo-eval wiring test Signed-off-by: Nikolay Petrov --- dokimi/tests/test_repo_eval_action_checkout.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/dokimi/tests/test_repo_eval_action_checkout.py b/dokimi/tests/test_repo_eval_action_checkout.py index d2f020c29..173b8d4e9 100644 --- a/dokimi/tests/test_repo_eval_action_checkout.py +++ b/dokimi/tests/test_repo_eval_action_checkout.py @@ -119,9 +119,8 @@ def resolve_ref(ref: str) -> str: def test_validate_installs_the_commit_it_resolved_when_the_repositories_match() -> None: - # Two ls-remote calls for one tag can name two commits if the tag moves between them. With one - # repository for the package and the action, the setup step gets the commit resolved above; a - # different package repository resolves its own ref, as before. + # Two ls-remote calls for one tag can name two commits if it moves between them: with one repository + # the setup step gets the commit resolved above. A different package repository resolves its own ref. setup = next(s for s in _validate_steps() if s.get("id") == "dokimi") assert setup["with"]["dokimi-ref"] == ( "${{ inputs.dokimi-repository == inputs.dokimi-action-repository" From c1f06d9635782c16b0071e21fe24ab7d0a831372 Mon Sep 17 00:00:00 2001 From: Anton Volkov Date: Sun, 1 Nov 2026 20:44:31 +0000 Subject: [PATCH 17/22] Accept an uppercase 40-digit SHA as dokimi-ref and lower-case it Signed-off-by: Anton Volkov --- .github/workflows/repo-eval.yml | 4 ++-- actions/setup/action.yml | 4 ++-- dokimi/tests/test_setup_action.py | 7 +++++++ 3 files changed, 11 insertions(+), 4 deletions(-) diff --git a/.github/workflows/repo-eval.yml b/.github/workflows/repo-eval.yml index bad3e3822..5a102deb3 100644 --- a/.github/workflows/repo-eval.yml +++ b/.github/workflows/repo-eval.yml @@ -139,8 +139,8 @@ jobs: exit 1 fi sha= - if [[ "$REF" =~ ^[0-9a-f]{40}$ ]]; then - sha=$REF + if [[ "$REF" =~ ^[0-9A-Fa-f]{40}$ ]]; then + sha=${REF,,} elif [[ "$REF" =~ ^[A-Za-z0-9][A-Za-z0-9._-]*$ ]]; then sha=$(git ls-remote --tags "https://github.com/$ACTION_REPO" \ | awk -v t="refs/tags/$REF" '$2 == t "^{}" { p = $1 } $2 == t { l = $1 } END { print (p != "" ? p : l) }') diff --git a/actions/setup/action.yml b/actions/setup/action.yml index e48bf946d..a6a44ce66 100644 --- a/actions/setup/action.yml +++ b/actions/setup/action.yml @@ -53,8 +53,8 @@ runs: echo "::error title=dokimi-repository::dokimi-repository is not owner/name" exit 1 fi - if [[ "$REF" =~ ^[0-9a-f]{40}$ ]]; then - sha=$REF + if [[ "$REF" =~ ^[0-9A-Fa-f]{40}$ ]]; then + sha=${REF,,} elif [[ "$REF" =~ ^[A-Za-z0-9][A-Za-z0-9._-]*$ ]]; then # An annotated tag's commit is its peeled (^{}) line. sha=$(git ls-remote --tags "https://github.com/$DOKIMI_REPO" \ diff --git a/dokimi/tests/test_setup_action.py b/dokimi/tests/test_setup_action.py index a6d25a1bc..dac432bbf 100644 --- a/dokimi/tests/test_setup_action.py +++ b/dokimi/tests/test_setup_action.py @@ -135,6 +135,13 @@ def test_a_commit_ref_is_installed_as_given_and_the_tag_is_not_looked_up_again(t assert output == f"sha={PEELED}\n" +def test_an_uppercase_commit_ref_is_lowercased(tmp_path: Path) -> None: + result, calls, output = _resolve_ref(tmp_path, PEELED.upper()) + assert result.returncode == 0, result.stderr + assert calls == "" + assert output == f"sha={PEELED}\n" + + def test_a_tag_ref_resolves_to_the_commit_it_names(tmp_path: Path) -> None: result, calls, output = _resolve_ref(tmp_path, "release") assert result.returncode == 0, result.stderr From 69312637f9eb3fe77b5bb281accd87bc829e707e Mon Sep 17 00:00:00 2001 From: Nikolay Petrov Date: Sun, 1 Nov 2026 13:09:53 -0800 Subject: [PATCH 18/22] test(ci): pin the setup wiring: package commit, no credentials, install step, uppercase resolver - scan and tasks-check install the package commit validate gated (dokimi-sha), not dokimi-ref. - Every actions/checkout in repo-eval.yml sets persist-credentials: false. - The Install dokimi step installs the resolved SHA, with --require-hashes for the lock and --no-deps for dokimi. - The repo-eval resolver lower-cases an uppercase 40-digit SHA, as action.yml does. Signed-off-by: Nikolay Petrov --- .../tests/test_repo_eval_action_checkout.py | 20 +++++++++++++++++++ dokimi/tests/test_setup_action.py | 8 ++++++++ 2 files changed, 28 insertions(+) diff --git a/dokimi/tests/test_repo_eval_action_checkout.py b/dokimi/tests/test_repo_eval_action_checkout.py index 173b8d4e9..1e0ae03d1 100644 --- a/dokimi/tests/test_repo_eval_action_checkout.py +++ b/dokimi/tests/test_repo_eval_action_checkout.py @@ -116,6 +116,7 @@ def resolve_ref(ref: str) -> str: assert resolve_ref("release") == "sha=2222222222222222222222222222222222222222\n" assert resolve_ref("3" * 40) == f"sha={'3' * 40}\n" + assert resolve_ref("B" * 40) == f"sha={'b' * 40}\n" def test_validate_installs_the_commit_it_resolved_when_the_repositories_match() -> None: @@ -125,3 +126,22 @@ def test_validate_installs_the_commit_it_resolved_when_the_repositories_match() assert setup["with"]["dokimi-ref"] == ( "${{ inputs.dokimi-repository == inputs.dokimi-action-repository" " && steps.action-ref.outputs.sha || inputs.dokimi-ref }}") + + +def test_scan_and_tasks_check_install_the_package_commit_validate_gated() -> None: + # The package SHA validate gated is the one the later jobs install: a tag in dokimi-ref may move meanwhile. + workflow = _load(REPO_EVAL) + for job_id in ("scan", "tasks-check"): + setup = [s for s in workflow["jobs"][job_id]["steps"] if s.get("uses") == f"./{CHECKOUT_PATH}/actions/setup"] + assert len(setup) == 1, job_id + assert setup[0]["with"]["dokimi-ref"] == "${{ needs.validate.outputs.dokimi-sha }}", job_id + assert setup[0]["with"]["dokimi-repository"] == "${{ inputs.dokimi-repository }}", job_id + + +def test_every_checkout_in_repo_eval_keeps_no_credentials() -> None: + # The checkouts sit in jobs that run the checked-out task code: the token must not stay in .git/config. + checkouts = [(job_id, step) for job_id, job in _load(REPO_EVAL)["jobs"].items() + for step in job.get("steps", []) if str(step.get("uses", "")).startswith("actions/checkout@")] + assert checkouts + for job_id, step in checkouts: + assert step.get("with", {}).get("persist-credentials") is False, job_id diff --git a/dokimi/tests/test_setup_action.py b/dokimi/tests/test_setup_action.py index dac432bbf..f3c1fe98f 100644 --- a/dokimi/tests/test_setup_action.py +++ b/dokimi/tests/test_setup_action.py @@ -162,3 +162,11 @@ def test_the_action_exports_no_dokimi_runs_dir_or_max_usd() -> None: text = ACTION.read_text(encoding="utf-8") assert "DOKIMI_RUNS_DIR" not in text assert "DOKIMI_MAX_USD" not in text + + +def test_the_install_step_installs_the_resolved_commit_with_its_locked_dependencies() -> None: + # Without the resolved SHA a moved tag installs another commit; without --require-hashes the lock is not verified. + install = next(s for s in _steps() if s.get("name") == "Install dokimi") + assert install["env"]["SHA"] == "${{ steps.ref.outputs.sha }}" + assert "pip install --require-hashes -r" in install["run"] + assert 'pip install --no-deps "dokimi @ git+' in install["run"] From 1de901d2aab1301c145fe712c08414e922d816eb Mon Sep 17 00:00:00 2001 From: Nikolay Petrov Date: Sun, 1 Nov 2026 13:10:31 -0800 Subject: [PATCH 19/22] test(ci): the wiring tests check needs: validate and the install URL uses the resolved SHA Signed-off-by: Nikolay Petrov --- dokimi/tests/test_repo_eval_action_checkout.py | 5 +++++ dokimi/tests/test_setup_action.py | 1 + 2 files changed, 6 insertions(+) diff --git a/dokimi/tests/test_repo_eval_action_checkout.py b/dokimi/tests/test_repo_eval_action_checkout.py index 1e0ae03d1..bf3aba3b3 100644 --- a/dokimi/tests/test_repo_eval_action_checkout.py +++ b/dokimi/tests/test_repo_eval_action_checkout.py @@ -27,6 +27,10 @@ def _load(path: Path) -> dict: return yaml.safe_load(path.read_text(encoding="utf-8")) +def _as_list(needs: str | list) -> list: + return [needs] if isinstance(needs, str) else needs + + def _action_checkouts(workflow: dict) -> list[tuple[str, dict]]: found = [] for job_id, job in workflow["jobs"].items(): @@ -134,6 +138,7 @@ def test_scan_and_tasks_check_install_the_package_commit_validate_gated() -> Non for job_id in ("scan", "tasks-check"): setup = [s for s in workflow["jobs"][job_id]["steps"] if s.get("uses") == f"./{CHECKOUT_PATH}/actions/setup"] assert len(setup) == 1, job_id + assert "validate" in _as_list(workflow["jobs"][job_id]["needs"]), job_id assert setup[0]["with"]["dokimi-ref"] == "${{ needs.validate.outputs.dokimi-sha }}", job_id assert setup[0]["with"]["dokimi-repository"] == "${{ inputs.dokimi-repository }}", job_id diff --git a/dokimi/tests/test_setup_action.py b/dokimi/tests/test_setup_action.py index f3c1fe98f..daa402080 100644 --- a/dokimi/tests/test_setup_action.py +++ b/dokimi/tests/test_setup_action.py @@ -170,3 +170,4 @@ def test_the_install_step_installs_the_resolved_commit_with_its_locked_dependenc assert install["env"]["SHA"] == "${{ steps.ref.outputs.sha }}" assert "pip install --require-hashes -r" in install["run"] assert 'pip install --no-deps "dokimi @ git+' in install["run"] + assert "@$SHA#subdirectory=dokimi" in install["run"] From ac84b384e92d3b312bbe86cf5528cda595d85041 Mon Sep 17 00:00:00 2001 From: Anton Volkov Date: Sun, 1 Nov 2026 22:21:59 +0000 Subject: [PATCH 20/22] Allow a slash in a tag name in both dokimi-ref resolvers Signed-off-by: Anton Volkov --- .github/workflows/repo-eval.yml | 2 +- actions/setup/action.yml | 2 +- dokimi/tests/test_setup_action.py | 9 ++++++++- 3 files changed, 10 insertions(+), 3 deletions(-) diff --git a/.github/workflows/repo-eval.yml b/.github/workflows/repo-eval.yml index 5a102deb3..2447297a7 100644 --- a/.github/workflows/repo-eval.yml +++ b/.github/workflows/repo-eval.yml @@ -141,7 +141,7 @@ jobs: sha= if [[ "$REF" =~ ^[0-9A-Fa-f]{40}$ ]]; then sha=${REF,,} - elif [[ "$REF" =~ ^[A-Za-z0-9][A-Za-z0-9._-]*$ ]]; then + elif [[ "$REF" =~ ^[A-Za-z0-9][A-Za-z0-9._/-]*$ ]]; then sha=$(git ls-remote --tags "https://github.com/$ACTION_REPO" \ | awk -v t="refs/tags/$REF" '$2 == t "^{}" { p = $1 } $2 == t { l = $1 } END { print (p != "" ? p : l) }') fi diff --git a/actions/setup/action.yml b/actions/setup/action.yml index a6a44ce66..ec02e2566 100644 --- a/actions/setup/action.yml +++ b/actions/setup/action.yml @@ -55,7 +55,7 @@ runs: fi if [[ "$REF" =~ ^[0-9A-Fa-f]{40}$ ]]; then sha=${REF,,} - elif [[ "$REF" =~ ^[A-Za-z0-9][A-Za-z0-9._-]*$ ]]; then + elif [[ "$REF" =~ ^[A-Za-z0-9][A-Za-z0-9._/-]*$ ]]; then # An annotated tag's commit is its peeled (^{}) line. sha=$(git ls-remote --tags "https://github.com/$DOKIMI_REPO" \ | awk -v t="refs/tags/$REF" '$2 == t "^{}" { p = $1 } $2 == t { l = $1 } END { print (p != "" ? p : l) }') diff --git a/dokimi/tests/test_setup_action.py b/dokimi/tests/test_setup_action.py index daa402080..f49239035 100644 --- a/dokimi/tests/test_setup_action.py +++ b/dokimi/tests/test_setup_action.py @@ -113,7 +113,7 @@ def _resolve_ref(tmp_path: Path, ref: str) -> tuple[subprocess.CompletedProcess[ fake.write_text( "#!/bin/sh\n" f"echo \"$*\" >> '{log}'\n" - f"printf '%s\\trefs/tags/release\\n%s\\trefs/tags/release^{{}}\\n' '{TAG_OBJECT}' '{PEELED}'\n", + f"printf '%s\\trefs/tags/release\\n%s\\trefs/tags/release^{{}}\\n%s\\trefs/tags/releases/v1\\n' '{TAG_OBJECT}' '{PEELED}' '{PEELED}'\n", encoding="utf-8") fake.chmod(0o755) output = tmp_path / "output" @@ -149,6 +149,13 @@ def test_a_tag_ref_resolves_to_the_commit_it_names(tmp_path: Path) -> None: assert output == f"sha={PEELED}\n" +def test_a_tag_ref_with_a_slash_resolves(tmp_path: Path) -> None: + result, calls, output = _resolve_ref(tmp_path, "releases/v1") + assert result.returncode == 0, result.stderr + assert "ls-remote --tags" in calls + assert output == f"sha={PEELED}\n" + + def test_the_action_declares_no_paid_or_max_usd_input() -> None: # A spending cap the action does not enforce must not ship as a safety input. inputs = yaml.safe_load(ACTION.read_text(encoding="utf-8"))["inputs"] From ef19fa00a3d74f5dea76be576975b904f21a2e56 Mon Sep 17 00:00:00 2001 From: Anton Volkov Date: Sun, 1 Nov 2026 22:52:15 +0000 Subject: [PATCH 21/22] Allow a plus in a tag name in both dokimi-ref resolvers Signed-off-by: Anton Volkov --- .github/workflows/repo-eval.yml | 2 +- actions/setup/action.yml | 2 +- dokimi/tests/test_setup_action.py | 9 ++++++++- 3 files changed, 10 insertions(+), 3 deletions(-) diff --git a/.github/workflows/repo-eval.yml b/.github/workflows/repo-eval.yml index 2447297a7..9c6a2e585 100644 --- a/.github/workflows/repo-eval.yml +++ b/.github/workflows/repo-eval.yml @@ -141,7 +141,7 @@ jobs: sha= if [[ "$REF" =~ ^[0-9A-Fa-f]{40}$ ]]; then sha=${REF,,} - elif [[ "$REF" =~ ^[A-Za-z0-9][A-Za-z0-9._/-]*$ ]]; then + elif [[ "$REF" =~ ^[A-Za-z0-9][A-Za-z0-9._/+-]*$ ]]; then sha=$(git ls-remote --tags "https://github.com/$ACTION_REPO" \ | awk -v t="refs/tags/$REF" '$2 == t "^{}" { p = $1 } $2 == t { l = $1 } END { print (p != "" ? p : l) }') fi diff --git a/actions/setup/action.yml b/actions/setup/action.yml index ec02e2566..6f856a04d 100644 --- a/actions/setup/action.yml +++ b/actions/setup/action.yml @@ -55,7 +55,7 @@ runs: fi if [[ "$REF" =~ ^[0-9A-Fa-f]{40}$ ]]; then sha=${REF,,} - elif [[ "$REF" =~ ^[A-Za-z0-9][A-Za-z0-9._/-]*$ ]]; then + elif [[ "$REF" =~ ^[A-Za-z0-9][A-Za-z0-9._/+-]*$ ]]; then # An annotated tag's commit is its peeled (^{}) line. sha=$(git ls-remote --tags "https://github.com/$DOKIMI_REPO" \ | awk -v t="refs/tags/$REF" '$2 == t "^{}" { p = $1 } $2 == t { l = $1 } END { print (p != "" ? p : l) }') diff --git a/dokimi/tests/test_setup_action.py b/dokimi/tests/test_setup_action.py index f49239035..0f637dfda 100644 --- a/dokimi/tests/test_setup_action.py +++ b/dokimi/tests/test_setup_action.py @@ -113,7 +113,7 @@ def _resolve_ref(tmp_path: Path, ref: str) -> tuple[subprocess.CompletedProcess[ fake.write_text( "#!/bin/sh\n" f"echo \"$*\" >> '{log}'\n" - f"printf '%s\\trefs/tags/release\\n%s\\trefs/tags/release^{{}}\\n%s\\trefs/tags/releases/v1\\n' '{TAG_OBJECT}' '{PEELED}' '{PEELED}'\n", + f"printf '%s\\trefs/tags/release\\n%s\\trefs/tags/release^{{}}\\n%s\\trefs/tags/releases/v1\\n%s\\trefs/tags/v1.2.3+build.1\\n' '{TAG_OBJECT}' '{PEELED}' '{PEELED}' '{PEELED}'\n", encoding="utf-8") fake.chmod(0o755) output = tmp_path / "output" @@ -156,6 +156,13 @@ def test_a_tag_ref_with_a_slash_resolves(tmp_path: Path) -> None: assert output == f"sha={PEELED}\n" +def test_a_tag_ref_with_a_plus_resolves(tmp_path: Path) -> None: + result, calls, output = _resolve_ref(tmp_path, "v1.2.3+build.1") + assert result.returncode == 0, result.stderr + assert "ls-remote --tags" in calls + assert output == f"sha={PEELED}\n" + + def test_the_action_declares_no_paid_or_max_usd_input() -> None: # A spending cap the action does not enforce must not ship as a safety input. inputs = yaml.safe_load(ACTION.read_text(encoding="utf-8"))["inputs"] From e94a00a41684ac479f80a2866353f957be4e7e16 Mon Sep 17 00:00:00 2001 From: Nikolay Petrov Date: Sun, 1 Nov 2026 17:51:29 -0800 Subject: [PATCH 22/22] test(setup): pin the full lock URL and the validate output dokimi-sha reads The install test accepted any lock URL, so /main/ passed. The checkout test did not pin where validate's dokimi-sha output comes from, so a mutation to the raw input passed. Both assertions were red on the mutations before this. Signed-off-by: Nikolay Petrov --- dokimi/tests/test_repo_eval_action_checkout.py | 2 ++ dokimi/tests/test_setup_action.py | 3 ++- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/dokimi/tests/test_repo_eval_action_checkout.py b/dokimi/tests/test_repo_eval_action_checkout.py index bf3aba3b3..511ba299c 100644 --- a/dokimi/tests/test_repo_eval_action_checkout.py +++ b/dokimi/tests/test_repo_eval_action_checkout.py @@ -141,6 +141,8 @@ def test_scan_and_tasks_check_install_the_package_commit_validate_gated() -> Non assert "validate" in _as_list(workflow["jobs"][job_id]["needs"]), job_id assert setup[0]["with"]["dokimi-ref"] == "${{ needs.validate.outputs.dokimi-sha }}", job_id assert setup[0]["with"]["dokimi-repository"] == "${{ inputs.dokimi-repository }}", job_id + # The output they read is what validate resolved and gated, not the raw input (a tag would move). + assert workflow["jobs"]["validate"]["outputs"]["dokimi-sha"] == "${{ steps.dokimi.outputs.sha }}" def test_every_checkout_in_repo_eval_keeps_no_credentials() -> None: diff --git a/dokimi/tests/test_setup_action.py b/dokimi/tests/test_setup_action.py index 0f637dfda..81a9fab7e 100644 --- a/dokimi/tests/test_setup_action.py +++ b/dokimi/tests/test_setup_action.py @@ -182,6 +182,7 @@ def test_the_install_step_installs_the_resolved_commit_with_its_locked_dependenc # Without the resolved SHA a moved tag installs another commit; without --require-hashes the lock is not verified. install = next(s for s in _steps() if s.get("name") == "Install dokimi") assert install["env"]["SHA"] == "${{ steps.ref.outputs.sha }}" - assert "pip install --require-hashes -r" in install["run"] + assert ('pip install --require-hashes -r "https://raw.githubusercontent.com/$DOKIMI_REPO/$SHA/' + 'dokimi/requirements.lock.txt"') in install["run"] assert 'pip install --no-deps "dokimi @ git+' in install["run"] assert "@$SHA#subdirectory=dokimi" in install["run"]