From 9061e9af4ead82ae0d36519d8411d5eee2354172 Mon Sep 17 00:00:00 2001 From: Wade Baglin Date: Sat, 29 Aug 2026 11:06:05 +1000 Subject: [PATCH 1/2] fix(ci): scrub OIDC variables in the automation process When the job grants `id-token: write`, the runner injects fresh OIDC request variables into each step process, so clearing them through GITHUB_ENV does not keep them out of the automation. Delete them from the github-script process instead, where the runtime and the Pi subprocess read them, and drop the separate confirmation step now that the check runs inline. --- .../actions/run-kipp-automation/action.yml | 31 ++++++++++--------- 1 file changed, 16 insertions(+), 15 deletions(-) diff --git a/.github/actions/run-kipp-automation/action.yml b/.github/actions/run-kipp-automation/action.yml index 9b8177d..69c85b4 100644 --- a/.github/actions/run-kipp-automation/action.yml +++ b/.github/actions/run-kipp-automation/action.yml @@ -18,7 +18,7 @@ inputs: required: false default: '' scrub-oidc-env: - description: 'Clear the GitHub OIDC request variables before the automation runs and for the remainder of the job. Enable only when no provider or later step needs OIDC. Default: `false`.' + description: 'Remove GitHub OIDC request variables from the automation process and clear them for later expression evaluation. Later steps may still receive fresh values when the job grants `id-token: write`. Enable only when the automation does not need OIDC. Default: `false`.' required: false default: 'false' @@ -81,9 +81,9 @@ runs: echo "Running automation '$AUTOMATION_ID' in a single step." fi - # GITHUB_ENV applies the empty values to the automation and every later - # step in the job. This is opt-in because some providers and downstream - # steps legitimately exchange the job's OIDC identity. + # Clearing these values through GITHUB_ENV affects later workflow expressions. When the job + # grants `id-token: write`, the runner still injects fresh values into each step process, so the + # automation process removes them again below. - name: Scrub OIDC request variables if: inputs.scrub-oidc-env == 'true' shell: bash @@ -94,17 +94,6 @@ runs: echo "ACTIONS_ID_TOKEN_REQUEST_TOKEN=" } >> "$GITHUB_ENV" - - name: Confirm OIDC request variables are scrubbed - if: inputs.scrub-oidc-env == 'true' - shell: bash - run: | - set -euo pipefail - if [ -n "${ACTIONS_ID_TOKEN_REQUEST_URL:-}" ] || [ -n "${ACTIONS_ID_TOKEN_REQUEST_TOKEN:-}" ]; then - echo "::error::The OIDC request variables are still set for the automation step. Refusing to run." - exit 1 - fi - echo "OIDC request variables are not visible to the automation step." - # Use github-script v9 or later because it runs on Node 24. The runtime # imports TypeScript automation modules and relies on Node 24's type stripping. # @@ -117,6 +106,7 @@ runs: KIPP_AUTOMATION_ID: ${{ inputs.automation-id }} KIPP_STAGE: ${{ inputs.stage }} KIPP_SUBJECT_PATH: ${{ inputs.subject-path }} + KIPP_SCRUB_OIDC_ENV: ${{ inputs.scrub-oidc-env }} # github-token configures github-script's Octokit client but does not # expose the token value. The runtime also needs the value for HTTPS Git # pushes and authenticated `gh` commands in the Pi subprocess. @@ -124,6 +114,17 @@ runs: with: github-token: ${{ inputs.github-token }} script: | + // Scrub before importing: the runtime reads this process environment, and the Pi + // subprocess inherits it. + const OIDC_REQUEST_VARIABLES = ["ACTIONS_ID_TOKEN_REQUEST_URL", "ACTIONS_ID_TOKEN_REQUEST_TOKEN"] + if (process.env.KIPP_SCRUB_OIDC_ENV === "true") { + for (const name of OIDC_REQUEST_VARIABLES) delete process.env[name] + const remaining = OIDC_REQUEST_VARIABLES.filter((name) => process.env[name] !== undefined) + if (remaining.length > 0) { + throw new Error("The OIDC request variables are still set for the automation step. Refusing to run.") + } + core.info("OIDC request variables are not visible to the automation step.") + } const { pathToFileURL } = await import("node:url") const runtime = await import(pathToFileURL(process.env.KIPP_RUNTIME_PATH).href) await runtime.runAutomation({ github, context, core, exec }) From a29720ef6cfa2fbd49f3a9377b9a0d9d9112f0d2 Mon Sep 17 00:00:00 2001 From: Wade Baglin Date: Sat, 29 Aug 2026 11:06:10 +1000 Subject: [PATCH 2/2] feat(ci): let setup-kipp use a local Kipp package Add a `package-path` input pointing at a Kipp package already present in the workspace. When it is set the action skips the registry download and reads the name and version from the package manifest, so a workflow can exercise an unpublished build. `kipp-version` is ignored in that mode. --- .github/actions/setup-kipp/action.yml | 72 ++++++++++++++++++++++++--- 1 file changed, 64 insertions(+), 8 deletions(-) diff --git a/.github/actions/setup-kipp/action.yml b/.github/actions/setup-kipp/action.yml index 89eee37..76cebbc 100644 --- a/.github/actions/setup-kipp/action.yml +++ b/.github/actions/setup-kipp/action.yml @@ -3,8 +3,12 @@ description: Install Kipp Managed Services and configure an isolated Pi environm inputs: kipp-version: - description: 'Exact Kipp version or `latest`.' + description: 'Exact Kipp version or `latest`. Ignored when `package-path` is set.' required: true + package-path: + description: 'Path to a local Kipp package, absolute or relative to `GITHUB_WORKSPACE`. When set, the action skips downloading Kipp from the registry and ignores `kipp-version`.' + required: false + default: '' ai-provider: description: 'AI provider to configure: `openai-compatible` or `azure-foundry`.' required: true @@ -42,10 +46,10 @@ inputs: outputs: kipp-version: - description: 'Exact version of the installed Kipp package.' + description: 'Exact version read from the selected Kipp package.' value: ${{ steps.download.outputs.kipp-version }} package-path: - description: 'Absolute path to the installed Kipp package root.' + description: 'Absolute path to the selected Kipp package root.' value: ${{ steps.download.outputs.package-path }} runtime-path: description: 'Absolute path to the Kipp automation runtime entry point.' @@ -64,12 +68,12 @@ runs: using: "composite" steps: - # Validate configuration before performing any installs. - name: Validate inputs id: validate shell: bash env: KIPP_VERSION: ${{ inputs.kipp-version }} + PACKAGE_PATH_INPUT: ${{ inputs.package-path }} AI_PROVIDER: ${{ inputs.ai-provider }} AI_MODEL: ${{ inputs.ai-model }} AI_API_KEY: ${{ inputs.ai-api-key }} @@ -94,6 +98,7 @@ runs: } reject_line_breaks "kipp-version" "$KIPP_VERSION" + reject_line_breaks "package-path" "$PACKAGE_PATH_INPUT" reject_line_breaks "ai-provider" "$AI_PROVIDER" reject_line_breaks "ai-model" "$AI_MODEL" reject_line_breaks "ai-api-key" "$AI_API_KEY" @@ -131,8 +136,19 @@ runs: *) fail "Unsupported ai-provider '$AI_PROVIDER'. Supported providers are: openai-compatible, azure-foundry." ;; esac - # Normal workflows pin an exact version. The self-maintenance workflows use `latest` to discover and propose version updates. - if [ "$KIPP_VERSION" != "latest" ]; then + PACKAGE_PATH='' + if [ -n "$PACKAGE_PATH_INPUT" ]; then + case "$PACKAGE_PATH_INPUT" in + /*) PACKAGE_PATH="$PACKAGE_PATH_INPUT" ;; + *) PACKAGE_PATH="$GITHUB_WORKSPACE/$PACKAGE_PATH_INPUT" ;; + esac + [ -d "$PACKAGE_PATH" ] || fail "package-path '$PACKAGE_PATH_INPUT' is not a directory at $PACKAGE_PATH." + [ -f "$PACKAGE_PATH/package.json" ] || fail "package-path '$PACKAGE_PATH_INPUT' has no package.json at $PACKAGE_PATH/package.json." + fi + + # Registry installs accept an exact version or `latest`. A local package supplies its own + # version through package.json, so kipp-version is ignored when package-path is set. + if [ -z "$PACKAGE_PATH" ] && [ "$KIPP_VERSION" != "latest" ]; then [[ "$KIPP_VERSION" =~ $EXACT_VERSION ]] || fail "kipp-version must be an exact version such as 1.2.3, or the literal 'latest', received '$KIPP_VERSION'." fi [ -n "$AI_MODEL" ] || fail "ai-model is required." @@ -157,6 +173,8 @@ runs: ;; esac + echo "package-path=$PACKAGE_PATH" >> "$GITHUB_OUTPUT" + echo "Provider '$AI_PROVIDER' selected with model '$AI_MODEL'." - name: Use Node.js ${{ inputs.node-version }} @@ -226,11 +244,50 @@ runs: PACKAGE_SCOPE: '@makerx-trusted-access' PACKAGE_NAME: '@makerx-trusted-access/kipp' KIPP_VERSION: ${{ inputs.kipp-version }} + PACKAGE_PATH_OVERRIDE: ${{ steps.validate.outputs.package-path }} run: | # zizmor: ignore[adhoc-packages] set -euo pipefail + if [ -n "$PACKAGE_PATH_OVERRIDE" ]; then + LOCAL_MANIFEST="$PACKAGE_PATH_OVERRIDE/package.json" + EXACT_VERSION='^[0-9]+\.[0-9]+\.[0-9]+(-[0-9A-Za-z.-]+)?(\+[0-9A-Za-z.-]+)?$' + + fail_manifest() { + echo "::error::The local Kipp package at $PACKAGE_PATH_OVERRIDE $1." + exit 1 + } + + # Normalize invalid field types so validation below reports consistent errors. + read_manifest_field() { + FIELD="$1" MANIFEST_PATH="$LOCAL_MANIFEST" node -e ' + const manifest = require(process.env.MANIFEST_PATH); + const value = manifest[process.env.FIELD]; + process.stdout.write(typeof value === "string" ? value : ""); + ' + } + + LOCAL_NAME="$(read_manifest_field name)" + LOCAL_VERSION="$(read_manifest_field version)" + + # Validate caller-controlled metadata before writing it to $GITHUB_OUTPUT or including it + # in a workflow command. The exact-version check also rejects line breaks. + [ -n "$LOCAL_NAME" ] || fail_manifest "declares no package name" + case "$LOCAL_NAME" in + *$'\r'*|*$'\n'*) fail_manifest "declares a package name containing a carriage return or newline" ;; + esac + [[ "$LOCAL_VERSION" =~ $EXACT_VERSION ]] || + fail_manifest "must declare an exact version such as 1.2.3, received '$LOCAL_VERSION'" + + { + echo "package-path=$PACKAGE_PATH_OVERRIDE" + echo "kipp-version=$LOCAL_VERSION" + } >> "$GITHUB_OUTPUT" + + echo "::notice::Using the local Kipp package $LOCAL_NAME@$LOCAL_VERSION at $PACKAGE_PATH_OVERRIDE. Nothing was installed from $REGISTRY_URL." + exit 0 + fi + NPMRC="$RUNNER_TEMP/kipp-trusted-access.npmrc" - # Delete the temporary npm credentials whether the step succeeds or fails. trap 'rm -f "$NPMRC"' EXIT if [ -z "${ACTIONS_ID_TOKEN_REQUEST_URL:-}" ] || [ -z "${ACTIONS_ID_TOKEN_REQUEST_TOKEN:-}" ]; then @@ -266,7 +323,6 @@ runs: "$PACKAGE_SCOPE" "$REGISTRY_URL" "$HOST" "$TOKEN" > "$NPMRC" ) export NPM_CONFIG_USERCONFIG="$NPMRC" - # If `latest` was requested, resolve it to an exact version before installing. INSTALL_VERSION="$KIPP_VERSION" if [ "$INSTALL_VERSION" = "latest" ]; then INSTALL_VERSION=$(npm view "$PACKAGE_NAME" version --registry "$REGISTRY_URL") || INSTALL_VERSION=""