mirror of
https://github.com/Twigpine/zero.git
synced 2026-11-08 17:56:14 -10:00
* feat(npm): publish as @twigpine/zero The npm package moves from @gitlawb/zero to @twigpine/zero, and everything that finds Zero's GitHub repository by name now names Twigpine/zero. Package name - package.json, the lockfile and release-please use @twigpine/zero. `repository` names Twigpine/zero: npm checks provenance against it, so a publish from this repo with the old URL fails with E422. The homepage is zero.twigpine.com. - The name lives in package.json only. build-platform-packages.mjs names the wrapper, platform versions, alias names and alias targets from it, and bin/zero.js reads its own package.json for the alias it resolves and for every reinstall hint. The Go constant npmPackageName is held equal to package.json by a test. Repository defaults - The update check (DefaultRepository), install.sh, install.ps1, the npm wrapper's fallback downloader, and the GitHub Action's zero-repo default all used Gitlawb/zero. That name now belongs to an unrelated GitHub org, and these paths download and run what they find there, so they point at Twigpine/zero. Docs, README and issue-template links follow. The Go module path (and so `go install github.com/Gitlawb/zero/...`) is unchanged here. Publishing - The two inline publish steps become scripts/npm/publish-packages.sh, which reads the name from the assembled payloads, refuses to start when the wrapper's aliases do not match the platform payloads, skips versions already on the registry (so re-running a half-finished job is safe), and publishes with --ignore-scripts. The dist-tag rules are unchanged. internal/npmwrapper/publish_test.go runs it against a fake registry. Docs explain the switch for existing installs: uninstall @gitlawb/zero first, because both packages own the `zero` bin and npm refuses the second (EEXIST). * feat(npm): publish 0.10.0 once more as @gitlawb/zero to point users at the new name Deprecating @gitlawb/zero alone would strand every existing npm install. 0.9.0's `zero upgrade` runs `npm install -g @gitlawb/zero@latest`; with nothing newer under that name npm installs nothing, prints no deprecation warning (it only warns when it installs something), and Zero then reports "updated via npm to 0.10.0" while staying on 0.9.0. So this release goes out once more under the old name, as a bridge: - The publish job assembles the same verified artifacts a second time with the new `build-platform-packages.mjs --package-name @gitlawb/zero` and publishes them with publish-packages.sh, after the @twigpine/zero publish. A gate runs it only while @gitlawb/zero's latest is still 0.9.0 (or is this version, on a re-run), so it happens once. - The build knows its name from the package.json beside it. The Go side reports InstallMethodNpmLegacy: `zero upgrade` refuses with the switch steps before any release check (so it works offline too), and `zero update --check` adds them whether or not an update is out. - bin/zero.js prints the same steps after zero exits (the TUI would hide a notice printed first), only when stderr is a terminal, and not for `update`/`upgrade`, which print it themselves. - Updating from 0.9.0 now installs a real version, so npm also shows the deprecation message. The steps say to uninstall @gitlawb/zero before installing @twigpine/zero: both own the `zero` bin, npm refuses the second install with EEXIST, and a forced install loses the bin when the old package is later removed (checked against npm 11.16). docs/NPM_PACKAGING.md has the maintainer runbook: bootstrap and trust the new package, retarget the old one's trusted publisher for the bridge, then deprecate @gitlawb/zero, revoke its publisher, and delete the bridge steps. * fix(npm): only skip a publish when npm says the version is not there publish-packages.sh treated any failed `npm view <name>@<version>` as "not published". A transient network, auth or registry error on the re-run that the skip exists for therefore published a version that was already out, and npm rejected it with EPUBLISHCONFLICT; with the registry unreachable it published blind. npm 11 reports every kind of missing (no such package, no such version, including platform versions) as E404, so only E404 now means "not published". Any other failure is retried three times and then stops the job before it publishes anything, with npm's own error shown. The test fake now answers a missing version with E404 the way npm does (it returned an empty success before), and can fail lookups with a network error. The two new tests fail on the old lookup: one with EPUBLISHCONFLICT after transient failures, the other by publishing while the registry is unreachable. Also renames the unused retry variable in the legacy-name gate step to `_` (shellcheck SC2034), matching the script.
297 lines
12 KiB
YAML
297 lines
12 KiB
YAML
name: ZERO
|
|
description: >-
|
|
Run the ZERO coding agent headlessly in a GitHub workflow. ZERO is
|
|
model- and provider-agnostic: you supply the provider and API key as inputs.
|
|
author: Twigpine
|
|
|
|
branding:
|
|
icon: terminal
|
|
color: green
|
|
|
|
inputs:
|
|
prompt:
|
|
description: The instruction for ZERO to execute. Mutually exclusive with prompt-file.
|
|
required: false
|
|
default: ""
|
|
prompt-file:
|
|
description: Path (relative to working-directory) to a file whose contents are the prompt.
|
|
required: false
|
|
default: ""
|
|
provider:
|
|
description: >-
|
|
Provider id to activate (for example openai, anthropic, gemini, ollama, or
|
|
any configured OpenAI-/Anthropic-compatible endpoint). Optional when the
|
|
repository already carries a .zero/config.json with an active provider.
|
|
required: false
|
|
default: ""
|
|
api-key:
|
|
description: >-
|
|
The provider API key. Pass it from a repository or organization secret;
|
|
it is exported only for the ZERO step and never written to the log.
|
|
required: false
|
|
default: ""
|
|
api-key-env:
|
|
description: >-
|
|
Name of the environment variable the chosen provider reads its key from
|
|
(for example OPENAI_API_KEY or ANTHROPIC_API_KEY). When set together with
|
|
api-key, that variable is exported only for the ZERO step. Leave the action
|
|
provider-agnostic by passing the env name your provider expects rather
|
|
than hardcoding one here.
|
|
required: false
|
|
default: ""
|
|
model:
|
|
description: Model id to use for the run. Defaults to the resolved provider's default.
|
|
required: false
|
|
default: ""
|
|
mode:
|
|
description: Run mode passed to `zero exec --mode` (for example smart, deep, or fast).
|
|
required: false
|
|
default: ""
|
|
auto:
|
|
description: >-
|
|
Autonomy ceiling passed to `zero exec --auto` (low, medium, or high).
|
|
Defaults to the conservative `low` so an unattended CI run cannot take
|
|
high-impact actions implicitly.
|
|
required: false
|
|
default: low
|
|
self-correct:
|
|
description: >-
|
|
When true, allow ZERO to request a stronger model mid-run if it hits a
|
|
wall (maps to `zero exec --allow-escalation`).
|
|
required: false
|
|
default: "false"
|
|
add-dir:
|
|
description: >-
|
|
Newline- or comma-separated extra write roots granted to the run (each
|
|
passed as `zero exec --add-dir`). The checked-out repository is always
|
|
writable; everything else stays read-only unless listed here.
|
|
required: false
|
|
default: ""
|
|
worktree:
|
|
description: When true, run inside an isolated git worktree (`zero exec --worktree`).
|
|
required: false
|
|
default: "false"
|
|
output-format:
|
|
description: >-
|
|
Output format for `zero exec` (text, json, or stream-json). Defaults to
|
|
stream-json, which is captured to a file and exposed as the output-file
|
|
output and uploaded by the caller if desired.
|
|
required: false
|
|
default: stream-json
|
|
post-to:
|
|
description: >-
|
|
Optional destination for a run summary after ZERO finishes: `pr-comment`
|
|
posts to the triggering pull request using github-token, `slack` posts to
|
|
slack-webhook-url, or `none` (default) posts nowhere.
|
|
required: false
|
|
default: none
|
|
slack-webhook-url:
|
|
description: >-
|
|
Slack incoming-webhook (or generic webhook) URL used when post-to is
|
|
slack. Pass it from a secret; it is never written to the log.
|
|
required: false
|
|
default: ""
|
|
github-token:
|
|
description: >-
|
|
Token used to post a PR comment when post-to is pr-comment. Defaults to
|
|
the workflow's GITHUB_TOKEN; the workflow must grant pull-requests: write.
|
|
required: false
|
|
default: ${{ github.token }}
|
|
working-directory:
|
|
description: Directory to run ZERO in. Defaults to the workspace root.
|
|
required: false
|
|
default: ${{ github.workspace }}
|
|
zero-version:
|
|
description: >-
|
|
ZERO release version/tag to install (for example v1.2.3 or latest).
|
|
Defaults to the ref this action was resolved at, falling back to latest.
|
|
required: false
|
|
default: ""
|
|
zero-repo:
|
|
description: Repository to install the ZERO release from (owner/repo).
|
|
required: false
|
|
default: Twigpine/zero
|
|
|
|
outputs:
|
|
exit-code:
|
|
description: The exit code ZERO returned (0 success, 2 usage, 3 provider, non-zero otherwise).
|
|
value: ${{ steps.run.outputs.exit-code }}
|
|
output-file:
|
|
description: Path to the captured ZERO stdout (the raw output-format stream).
|
|
value: ${{ steps.run.outputs.output-file }}
|
|
summary:
|
|
description: A short human-readable summary line parsed from the run, when available.
|
|
value: ${{ steps.run.outputs.summary }}
|
|
|
|
runs:
|
|
using: composite
|
|
steps:
|
|
- name: Install ZERO
|
|
shell: bash
|
|
env:
|
|
ZERO_REPO: ${{ inputs.zero-repo }}
|
|
ZERO_VERSION_INPUT: ${{ inputs.zero-version }}
|
|
ACTION_REF: ${{ github.action_ref }}
|
|
ACTION_PATH: ${{ github.action_path }}
|
|
RUNNER_TEMP: ${{ runner.temp }}
|
|
run: |
|
|
set -euo pipefail
|
|
if [ "${RUNNER_OS}" = "Windows" ]; then
|
|
echo "::error::The ZERO action currently supports Linux and macOS runners only." >&2
|
|
exit 1
|
|
fi
|
|
# Resolve the version to install: explicit input wins; otherwise the ref
|
|
# this action was checked out at (a tag); otherwise the latest release.
|
|
version="${ZERO_VERSION_INPUT}"
|
|
if [ -z "${version}" ]; then
|
|
version="${ACTION_REF}"
|
|
fi
|
|
# A commit-SHA pin (github.action_ref is the 40-hex SHA) is not a release
|
|
# tag, so fall back to latest rather than fetching releases/download/v<sha>.
|
|
case "${version}" in
|
|
""|refs/*|*/*) version="latest" ;;
|
|
esac
|
|
if printf '%s' "${version}" | grep -qiE '^[0-9a-f]{40}$'; then
|
|
version="latest"
|
|
fi
|
|
install_dir="${RUNNER_TEMP}/zero-bin"
|
|
mkdir -p "${install_dir}"
|
|
# Reuse ZERO's own checksum-verifying installer, shipped with the action.
|
|
ZERO_REPO="${ZERO_REPO}" ZERO_VERSION="${version}" ZERO_INSTALL_DIR="${install_dir}" \
|
|
bash "${ACTION_PATH}/scripts/install.sh"
|
|
echo "${install_dir}" >> "${GITHUB_PATH}"
|
|
|
|
- name: Run ZERO
|
|
id: run
|
|
shell: bash
|
|
working-directory: ${{ inputs.working-directory }}
|
|
env:
|
|
# Secrets are passed via the environment and never echoed. `set +x` and
|
|
# the absence of any `echo` of these values keep them out of the log.
|
|
ZERO_INPUT_API_KEY: ${{ inputs.api-key }}
|
|
ZERO_INPUT_API_KEY_ENV: ${{ inputs.api-key-env }}
|
|
ZERO_INPUT_PROVIDER: ${{ inputs.provider }}
|
|
ZERO_INPUT_PROMPT: ${{ inputs.prompt }}
|
|
ZERO_INPUT_PROMPT_FILE: ${{ inputs.prompt-file }}
|
|
ZERO_INPUT_MODEL: ${{ inputs.model }}
|
|
ZERO_INPUT_MODE: ${{ inputs.mode }}
|
|
ZERO_INPUT_AUTO: ${{ inputs.auto }}
|
|
ZERO_INPUT_SELF_CORRECT: ${{ inputs.self-correct }}
|
|
ZERO_INPUT_ADD_DIR: ${{ inputs.add-dir }}
|
|
ZERO_INPUT_WORKTREE: ${{ inputs.worktree }}
|
|
ZERO_INPUT_OUTPUT_FORMAT: ${{ inputs.output-format }}
|
|
ACTION_PATH: ${{ github.action_path }}
|
|
RUNNER_TEMP: ${{ runner.temp }}
|
|
run: |
|
|
set -euo pipefail
|
|
|
|
# Export the provider key under the env name the provider expects. This
|
|
# keeps the action provider-agnostic: no provider->env mapping is baked
|
|
# in. The value is never printed.
|
|
if [ -n "${ZERO_INPUT_API_KEY}" ] && [ -n "${ZERO_INPUT_API_KEY_ENV}" ]; then
|
|
if ! printf '%s' "${ZERO_INPUT_API_KEY_ENV}" | grep -qE '^[A-Za-z_][A-Za-z0-9_]*$'; then
|
|
echo "::error::api-key-env must be a valid environment variable name." >&2
|
|
exit 2
|
|
fi
|
|
# Export in-process for this Run ZERO step so zero can read it; do
|
|
# not persist provider credentials to later job steps.
|
|
export "${ZERO_INPUT_API_KEY_ENV}=${ZERO_INPUT_API_KEY}"
|
|
fi
|
|
if [ -n "${ZERO_INPUT_PROVIDER}" ]; then
|
|
export ZERO_PROVIDER="${ZERO_INPUT_PROVIDER}"
|
|
fi
|
|
|
|
args=(exec)
|
|
|
|
if [ -n "${ZERO_INPUT_PROMPT_FILE}" ] && [ -n "${ZERO_INPUT_PROMPT}" ]; then
|
|
echo "::error::The 'prompt' and 'prompt-file' inputs are mutually exclusive." >&2
|
|
exit 2
|
|
fi
|
|
if [ -n "${ZERO_INPUT_PROMPT_FILE}" ]; then
|
|
args+=(--file "${ZERO_INPUT_PROMPT_FILE}")
|
|
elif [ -n "${ZERO_INPUT_PROMPT}" ]; then
|
|
args+=(--prompt "${ZERO_INPUT_PROMPT}")
|
|
else
|
|
echo "::error::Provide either the 'prompt' or 'prompt-file' input." >&2
|
|
exit 2
|
|
fi
|
|
|
|
[ -n "${ZERO_INPUT_MODEL}" ] && args+=(--model "${ZERO_INPUT_MODEL}")
|
|
[ -n "${ZERO_INPUT_MODE}" ] && args+=(--mode "${ZERO_INPUT_MODE}")
|
|
[ -n "${ZERO_INPUT_AUTO}" ] && args+=(--auto "${ZERO_INPUT_AUTO}")
|
|
[ "${ZERO_INPUT_SELF_CORRECT}" = "true" ] && args+=(--allow-escalation)
|
|
[ "${ZERO_INPUT_WORKTREE}" = "true" ] && args+=(--worktree)
|
|
args+=(--output-format "${ZERO_INPUT_OUTPUT_FORMAT}")
|
|
# No --notify in CI (no terminal); the optional Slack post is handled
|
|
# separately below so it works regardless of output format.
|
|
args+=(--no-notify)
|
|
|
|
# Split add-dir on newlines and commas; each non-empty entry is a root.
|
|
if [ -n "${ZERO_INPUT_ADD_DIR}" ]; then
|
|
while IFS= read -r dir; do
|
|
# Trim only surrounding whitespace so paths with internal spaces survive.
|
|
dir="$(printf '%s' "${dir}" | sed 's/^[[:space:]]*//; s/[[:space:]]*$//')"
|
|
[ -n "${dir}" ] && args+=(--add-dir "${dir}")
|
|
done < <(printf '%s' "${ZERO_INPUT_ADD_DIR}" | tr ',' '\n')
|
|
fi
|
|
|
|
output_file="${RUNNER_TEMP}/zero-output.txt"
|
|
|
|
# Run ZERO. Capture stdout to a file AND echo it to the log. Surface the
|
|
# real exit code as the step's status. The sandbox is active by default;
|
|
# this action never enables unsafe permission-skipping.
|
|
set +e
|
|
zero "${args[@]}" | tee "${output_file}"
|
|
code=${PIPESTATUS[0]}
|
|
set -e
|
|
|
|
echo "exit-code=${code}" >> "${GITHUB_OUTPUT}"
|
|
echo "output-file=${output_file}" >> "${GITHUB_OUTPUT}"
|
|
|
|
# Best-effort one-line summary. Structured output ends with lifecycle
|
|
# metadata, so parse final/error events instead of tailing stdout.
|
|
summary="$(node "${ACTION_PATH}/scripts/action-summary.mjs" "${ZERO_INPUT_OUTPUT_FORMAT}" "${output_file}" || true)"
|
|
{
|
|
echo "summary<<ZERO_EOF"
|
|
printf '%s\n' "${summary}"
|
|
echo "ZERO_EOF"
|
|
} >> "${GITHUB_OUTPUT}"
|
|
|
|
exit "${code}"
|
|
|
|
- name: Post run summary to pull request
|
|
if: ${{ always() && inputs.post-to == 'pr-comment' && github.event.pull_request.number != '' }}
|
|
shell: bash
|
|
env:
|
|
GH_TOKEN: ${{ inputs.github-token }}
|
|
PR_NUMBER: ${{ github.event.pull_request.number }}
|
|
ZERO_EXIT: ${{ steps.run.outputs.exit-code }}
|
|
ZERO_SUMMARY: ${{ steps.run.outputs.summary }}
|
|
run: |
|
|
set -euo pipefail
|
|
status="succeeded"
|
|
[ "${ZERO_EXIT}" != "0" ] && status="failed (exit ${ZERO_EXIT})"
|
|
body="$(printf 'ZERO run %s.\n\n%s' "${status}" "${ZERO_SUMMARY}")"
|
|
gh api "repos/${GITHUB_REPOSITORY}/issues/${PR_NUMBER}/comments" \
|
|
-f body="${body}" >/dev/null
|
|
|
|
- name: Post run summary to Slack
|
|
if: ${{ always() && inputs.post-to == 'slack' && inputs.slack-webhook-url != '' }}
|
|
shell: bash
|
|
env:
|
|
SLACK_WEBHOOK_URL: ${{ inputs.slack-webhook-url }}
|
|
ZERO_EXIT: ${{ steps.run.outputs.exit-code }}
|
|
ZERO_SUMMARY: ${{ steps.run.outputs.summary }}
|
|
RUN_URL: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}
|
|
run: |
|
|
set -euo pipefail
|
|
status="succeeded"
|
|
[ "${ZERO_EXIT}" != "0" ] && status="failed (exit ${ZERO_EXIT})"
|
|
text="ZERO run ${status} for ${GITHUB_REPOSITORY}@${GITHUB_REF_NAME}. ${ZERO_SUMMARY} ${RUN_URL}"
|
|
# Slack incoming webhooks accept {"text": "..."}; jq escapes the body and
|
|
# the URL is never echoed. A delivery failure is a warning, not a failure.
|
|
payload="$(jq -n --arg text "${text}" '{text: $text}')"
|
|
curl --fail --silent --show-error -X POST -H 'Content-Type: application/json' \
|
|
-d "${payload}" "${SLACK_WEBHOOK_URL}" >/dev/null || \
|
|
echo "::warning::Slack webhook delivery failed (run not affected)."
|