mirror of
https://github.com/github/spec-kit.git
synced 2026-07-04 04:45:43 +08:00
Compare commits
1 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9f1be37039 |
46
.github/workflows/add-community-extension.lock.yml
generated
vendored
46
.github/workflows/add-community-extension.lock.yml
generated
vendored
@@ -1,4 +1,4 @@
|
||||
# gh-aw-metadata: {"schema_version":"v3","frontmatter_hash":"f1073a236eb41f9fc2b5b8c1e58c25e02b5a6d18d242887636acc9007dd1542e","compiler_version":"v0.74.8","strict":true,"agent_id":"copilot"}
|
||||
# gh-aw-metadata: {"schema_version":"v3","frontmatter_hash":"1583d46477aa35f00e2c7ab97f06bacf4f6e21bdaa9d58b0ef704a588e588a7e","compiler_version":"v0.74.8","strict":true,"agent_id":"copilot"}
|
||||
# gh-aw-manifest: {"version":1,"secrets":["COPILOT_GITHUB_TOKEN","GH_AW_CI_TRIGGER_TOKEN","GH_AW_GITHUB_MCP_SERVER_TOKEN","GH_AW_GITHUB_TOKEN","GITHUB_TOKEN"],"actions":[{"repo":"actions/checkout","sha":"de0fac2e4500dabe0009e67214ff5f5447ce83dd","version":"v6.0.2"},{"repo":"actions/download-artifact","sha":"3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c","version":"v8.0.1"},{"repo":"actions/github-script","sha":"3a2844b7e9c422d3c10d287c895573f7108da1b3","version":"v9.0.0"},{"repo":"actions/setup-node","sha":"48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e","version":"v6.4.0"},{"repo":"actions/upload-artifact","sha":"043fb46d1a93c77aae656e7c1c64a875d1fc6a0a","version":"v7.0.1"},{"repo":"github/gh-aw-actions/setup","sha":"efa55847f72aadb03490d955263ff911bf758700","version":"v0.74.8"}],"containers":[{"image":"ghcr.io/github/gh-aw-firewall/agent:0.25.49"},{"image":"ghcr.io/github/gh-aw-firewall/api-proxy:0.25.49"},{"image":"ghcr.io/github/gh-aw-firewall/squid:0.25.49"},{"image":"ghcr.io/github/gh-aw-mcpg:v0.3.9","digest":"sha256:64828b42a4482f58fab16509d7f8f495a6d97c972a98a68aff20543531ac0388","pinned_image":"ghcr.io/github/gh-aw-mcpg:v0.3.9@sha256:64828b42a4482f58fab16509d7f8f495a6d97c972a98a68aff20543531ac0388"},{"image":"ghcr.io/github/github-mcp-server:v1.0.4"},{"image":"node:lts-alpine","digest":"sha256:d1b3b4da11eefd5941e7f0b9cf17783fc99d9c6fc34884a665f40a06dbdfc94f","pinned_image":"node:lts-alpine@sha256:d1b3b4da11eefd5941e7f0b9cf17783fc99d9c6fc34884a665f40a06dbdfc94f"}]}
|
||||
# ___ _ _
|
||||
# / _ \ | | (_)
|
||||
@@ -38,7 +38,7 @@
|
||||
# - actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 (source v9)
|
||||
# - actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
|
||||
# - actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
|
||||
# - github/gh-aw-actions/setup@b11be78086764c43fa463398aed7ffdcf40549c1 # v0.77.0
|
||||
# - github/gh-aw-actions/setup@318d7f4901f78b85e25b91709cf0109ac9b425f6 # v0.74.9
|
||||
#
|
||||
# Container images used:
|
||||
# - ghcr.io/github/gh-aw-firewall/agent:0.25.49
|
||||
@@ -52,6 +52,8 @@ name: "Add Community Extension from Issue Submission"
|
||||
on:
|
||||
issues:
|
||||
types:
|
||||
- opened
|
||||
- edited
|
||||
- labeled
|
||||
# skip-bots: # Skip-bots processed as bot check in pre-activation job
|
||||
# - github-actions # Skip-bots processed as bot check in pre-activation job
|
||||
@@ -90,7 +92,7 @@ jobs:
|
||||
steps:
|
||||
- name: Setup Scripts
|
||||
id: setup
|
||||
uses: github/gh-aw-actions/setup@b11be78086764c43fa463398aed7ffdcf40549c1 # v0.77.0
|
||||
uses: github/gh-aw-actions/setup@318d7f4901f78b85e25b91709cf0109ac9b425f6 # v0.74.9
|
||||
with:
|
||||
destination: ${{ runner.temp }}/gh-aw/actions
|
||||
job-name: ${{ github.job }}
|
||||
@@ -204,23 +206,23 @@ jobs:
|
||||
run: |
|
||||
bash "${RUNNER_TEMP}/gh-aw/actions/create_prompt_first.sh"
|
||||
{
|
||||
cat << 'GH_AW_PROMPT_25355d452b4d239a_EOF'
|
||||
cat << 'GH_AW_PROMPT_2b92c540a0b471a7_EOF'
|
||||
<system>
|
||||
GH_AW_PROMPT_25355d452b4d239a_EOF
|
||||
GH_AW_PROMPT_2b92c540a0b471a7_EOF
|
||||
cat "${RUNNER_TEMP}/gh-aw/prompts/xpia.md"
|
||||
cat "${RUNNER_TEMP}/gh-aw/prompts/temp_folder_prompt.md"
|
||||
cat "${RUNNER_TEMP}/gh-aw/prompts/markdown.md"
|
||||
cat "${RUNNER_TEMP}/gh-aw/prompts/safe_outputs_prompt.md"
|
||||
cat << 'GH_AW_PROMPT_25355d452b4d239a_EOF'
|
||||
cat << 'GH_AW_PROMPT_2b92c540a0b471a7_EOF'
|
||||
<safe-output-tools>
|
||||
Tools: add_comment(max:2), create_pull_request, add_labels(max:3), missing_tool, missing_data, noop
|
||||
GH_AW_PROMPT_25355d452b4d239a_EOF
|
||||
GH_AW_PROMPT_2b92c540a0b471a7_EOF
|
||||
cat "${RUNNER_TEMP}/gh-aw/prompts/safe_outputs_create_pull_request.md"
|
||||
cat << 'GH_AW_PROMPT_25355d452b4d239a_EOF'
|
||||
cat << 'GH_AW_PROMPT_2b92c540a0b471a7_EOF'
|
||||
</safe-output-tools>
|
||||
GH_AW_PROMPT_25355d452b4d239a_EOF
|
||||
GH_AW_PROMPT_2b92c540a0b471a7_EOF
|
||||
cat "${RUNNER_TEMP}/gh-aw/prompts/mcp_cli_tools_prompt.md"
|
||||
cat << 'GH_AW_PROMPT_25355d452b4d239a_EOF'
|
||||
cat << 'GH_AW_PROMPT_2b92c540a0b471a7_EOF'
|
||||
<github-context>
|
||||
The following GitHub context information is available for this workflow:
|
||||
{{#if github.actor}}
|
||||
@@ -252,12 +254,12 @@ jobs:
|
||||
- **Note**: If a branch you need is not in the list above and is not listed as an additional fetched ref, it has NOT been checked out. For private repositories you cannot fetch it without proper authentication. If the branch is required and not available, exit with an error and ask the user to add it to the `fetch:` option of the `checkout:` configuration (e.g., `fetch: ["refs/pulls/open/*"]` for all open PR refs, or `fetch: ["main", "feature/my-branch"]` for specific branches).
|
||||
</github-context>
|
||||
|
||||
GH_AW_PROMPT_25355d452b4d239a_EOF
|
||||
GH_AW_PROMPT_2b92c540a0b471a7_EOF
|
||||
cat "${RUNNER_TEMP}/gh-aw/prompts/github_mcp_tools_with_safeoutputs_prompt.md"
|
||||
cat << 'GH_AW_PROMPT_25355d452b4d239a_EOF'
|
||||
cat << 'GH_AW_PROMPT_2b92c540a0b471a7_EOF'
|
||||
</system>
|
||||
{{#runtime-import .github/workflows/add-community-extension.md}}
|
||||
GH_AW_PROMPT_25355d452b4d239a_EOF
|
||||
GH_AW_PROMPT_2b92c540a0b471a7_EOF
|
||||
} > "$GH_AW_PROMPT"
|
||||
- name: Interpolate variables and render templates
|
||||
uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
|
||||
@@ -368,7 +370,7 @@ jobs:
|
||||
steps:
|
||||
- name: Setup Scripts
|
||||
id: setup
|
||||
uses: github/gh-aw-actions/setup@b11be78086764c43fa463398aed7ffdcf40549c1 # v0.77.0
|
||||
uses: github/gh-aw-actions/setup@318d7f4901f78b85e25b91709cf0109ac9b425f6 # v0.74.9
|
||||
with:
|
||||
destination: ${{ runner.temp }}/gh-aw/actions
|
||||
job-name: ${{ github.job }}
|
||||
@@ -464,9 +466,9 @@ jobs:
|
||||
mkdir -p "${RUNNER_TEMP}/gh-aw/safeoutputs"
|
||||
mkdir -p /tmp/gh-aw/safeoutputs
|
||||
mkdir -p /tmp/gh-aw/mcp-logs/safeoutputs
|
||||
cat > "${RUNNER_TEMP}/gh-aw/safeoutputs/config.json" << 'GH_AW_SAFE_OUTPUTS_CONFIG_a6227a6d6ade9e30_EOF'
|
||||
cat > "${RUNNER_TEMP}/gh-aw/safeoutputs/config.json" << 'GH_AW_SAFE_OUTPUTS_CONFIG_66c58b0f685caa27_EOF'
|
||||
{"add_comment":{"max":2},"add_labels":{"allowed":["extension-submission","validation-passed","validation-failed","needs-info"],"max":3},"create_pull_request":{"draft":true,"labels":["extension-submission","automated"],"max":1,"max_patch_files":100,"max_patch_size":1024,"protect_top_level_dot_folders":true,"protected_files":["package.json","bun.lockb","bunfig.toml","deno.json","deno.jsonc","deno.lock","global.json","NuGet.Config","Directory.Packages.props","mix.exs","mix.lock","go.mod","go.sum","stack.yaml","stack.yaml.lock","pom.xml","build.gradle","build.gradle.kts","settings.gradle","settings.gradle.kts","gradle.properties","package-lock.json","yarn.lock","pnpm-lock.yaml","npm-shrinkwrap.json","requirements.txt","Pipfile","Pipfile.lock","pyproject.toml","setup.py","setup.cfg","Gemfile","Gemfile.lock","uv.lock","CODEOWNERS","DESIGN.md","CONTRIBUTING.md","SECURITY.md","CODE_OF_CONDUCT.md","AGENTS.md","CLAUDE.md","GEMINI.md"],"protected_files_policy":"blocked","title_prefix":"[extension] "},"create_report_incomplete_issue":{},"missing_data":{},"missing_tool":{},"noop":{"max":1,"report-as-issue":"true"},"report_incomplete":{}}
|
||||
GH_AW_SAFE_OUTPUTS_CONFIG_a6227a6d6ade9e30_EOF
|
||||
GH_AW_SAFE_OUTPUTS_CONFIG_66c58b0f685caa27_EOF
|
||||
- name: Generate Safe Outputs Tools
|
||||
env:
|
||||
GH_AW_TOOLS_META_JSON: |
|
||||
@@ -722,7 +724,7 @@ jobs:
|
||||
|
||||
mkdir -p /home/runner/.copilot
|
||||
GH_AW_NODE=$(which node 2>/dev/null || command -v node 2>/dev/null || echo node)
|
||||
cat << GH_AW_MCP_CONFIG_6ce4129d4503180e_EOF | "$GH_AW_NODE" "${RUNNER_TEMP}/gh-aw/actions/start_mcp_gateway.cjs"
|
||||
cat << GH_AW_MCP_CONFIG_881a93100a972629_EOF | "$GH_AW_NODE" "${RUNNER_TEMP}/gh-aw/actions/start_mcp_gateway.cjs"
|
||||
{
|
||||
"mcpServers": {
|
||||
"github": {
|
||||
@@ -763,7 +765,7 @@ jobs:
|
||||
"payloadDir": "${MCP_GATEWAY_PAYLOAD_DIR}"
|
||||
}
|
||||
}
|
||||
GH_AW_MCP_CONFIG_6ce4129d4503180e_EOF
|
||||
GH_AW_MCP_CONFIG_881a93100a972629_EOF
|
||||
- name: Mount MCP servers as CLIs
|
||||
id: mount-mcp-clis
|
||||
continue-on-error: true
|
||||
@@ -1045,7 +1047,7 @@ jobs:
|
||||
steps:
|
||||
- name: Setup Scripts
|
||||
id: setup
|
||||
uses: github/gh-aw-actions/setup@b11be78086764c43fa463398aed7ffdcf40549c1 # v0.77.0
|
||||
uses: github/gh-aw-actions/setup@318d7f4901f78b85e25b91709cf0109ac9b425f6 # v0.74.9
|
||||
with:
|
||||
destination: ${{ runner.temp }}/gh-aw/actions
|
||||
job-name: ${{ github.job }}
|
||||
@@ -1186,7 +1188,7 @@ jobs:
|
||||
steps:
|
||||
- name: Setup Scripts
|
||||
id: setup
|
||||
uses: github/gh-aw-actions/setup@b11be78086764c43fa463398aed7ffdcf40549c1 # v0.77.0
|
||||
uses: github/gh-aw-actions/setup@318d7f4901f78b85e25b91709cf0109ac9b425f6 # v0.74.9
|
||||
with:
|
||||
destination: ${{ runner.temp }}/gh-aw/actions
|
||||
job-name: ${{ github.job }}
|
||||
@@ -1382,7 +1384,7 @@ jobs:
|
||||
steps:
|
||||
- name: Setup Scripts
|
||||
id: setup
|
||||
uses: github/gh-aw-actions/setup@b11be78086764c43fa463398aed7ffdcf40549c1 # v0.77.0
|
||||
uses: github/gh-aw-actions/setup@318d7f4901f78b85e25b91709cf0109ac9b425f6 # v0.74.9
|
||||
with:
|
||||
destination: ${{ runner.temp }}/gh-aw/actions
|
||||
job-name: ${{ github.job }}
|
||||
@@ -1454,7 +1456,7 @@ jobs:
|
||||
steps:
|
||||
- name: Setup Scripts
|
||||
id: setup
|
||||
uses: github/gh-aw-actions/setup@b11be78086764c43fa463398aed7ffdcf40549c1 # v0.77.0
|
||||
uses: github/gh-aw-actions/setup@318d7f4901f78b85e25b91709cf0109ac9b425f6 # v0.74.9
|
||||
with:
|
||||
destination: ${{ runner.temp }}/gh-aw/actions
|
||||
job-name: ${{ github.job }}
|
||||
|
||||
15
.github/workflows/add-community-extension.md
vendored
15
.github/workflows/add-community-extension.md
vendored
@@ -4,7 +4,7 @@ emoji: "🧩"
|
||||
|
||||
on:
|
||||
issues:
|
||||
types: [labeled]
|
||||
types: [opened, edited, labeled]
|
||||
skip-bots: [github-actions, copilot, dependabot]
|
||||
|
||||
tools:
|
||||
@@ -22,8 +22,6 @@ checkout:
|
||||
fetch-depth: 0
|
||||
|
||||
safe-outputs:
|
||||
noop:
|
||||
report-as-issue: false
|
||||
create-pull-request:
|
||||
title-prefix: "[extension] "
|
||||
labels: [extension-submission, automated]
|
||||
@@ -49,9 +47,14 @@ or update entries in the community extension catalog.
|
||||
|
||||
## Triggering Conditions
|
||||
|
||||
This workflow only triggers when the `extension-submission` label is added to an
|
||||
issue. Before processing, verify that the issue title starts with `[Extension]:`.
|
||||
If it does not, stop without commenting.
|
||||
This workflow triggers on issue events. **Only process the issue if ALL of these
|
||||
conditions are met:**
|
||||
|
||||
1. The issue has the `extension-submission` label
|
||||
2. The issue title starts with `[Extension]:`
|
||||
|
||||
If the issue does not meet these conditions, add a brief comment explaining that
|
||||
this workflow only processes extension submission issues, then stop.
|
||||
|
||||
## Step 1 — Read and Parse the Issue
|
||||
|
||||
|
||||
46
.github/workflows/add-community-preset.lock.yml
generated
vendored
46
.github/workflows/add-community-preset.lock.yml
generated
vendored
@@ -1,4 +1,4 @@
|
||||
# gh-aw-metadata: {"schema_version":"v3","frontmatter_hash":"f6cbeb7bc3ee4de1c2b3963fbf21525d0add0425a6807a8335f8f9d93e01a44f","compiler_version":"v0.74.8","strict":true,"agent_id":"copilot"}
|
||||
# gh-aw-metadata: {"schema_version":"v3","frontmatter_hash":"6111bd4a1cd2c363f1f05f185164e08883d6df79da732a8c07b9aa602ed7dfe6","compiler_version":"v0.74.8","strict":true,"agent_id":"copilot"}
|
||||
# gh-aw-manifest: {"version":1,"secrets":["COPILOT_GITHUB_TOKEN","GH_AW_CI_TRIGGER_TOKEN","GH_AW_GITHUB_MCP_SERVER_TOKEN","GH_AW_GITHUB_TOKEN","GITHUB_TOKEN"],"actions":[{"repo":"actions/checkout","sha":"de0fac2e4500dabe0009e67214ff5f5447ce83dd","version":"v6.0.2"},{"repo":"actions/download-artifact","sha":"3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c","version":"v8.0.1"},{"repo":"actions/github-script","sha":"3a2844b7e9c422d3c10d287c895573f7108da1b3","version":"v9.0.0"},{"repo":"actions/setup-node","sha":"48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e","version":"v6.4.0"},{"repo":"actions/upload-artifact","sha":"043fb46d1a93c77aae656e7c1c64a875d1fc6a0a","version":"v7.0.1"},{"repo":"github/gh-aw-actions/setup","sha":"efa55847f72aadb03490d955263ff911bf758700","version":"v0.74.8"}],"containers":[{"image":"ghcr.io/github/gh-aw-firewall/agent:0.25.49"},{"image":"ghcr.io/github/gh-aw-firewall/api-proxy:0.25.49"},{"image":"ghcr.io/github/gh-aw-firewall/squid:0.25.49"},{"image":"ghcr.io/github/gh-aw-mcpg:v0.3.9","digest":"sha256:64828b42a4482f58fab16509d7f8f495a6d97c972a98a68aff20543531ac0388","pinned_image":"ghcr.io/github/gh-aw-mcpg:v0.3.9@sha256:64828b42a4482f58fab16509d7f8f495a6d97c972a98a68aff20543531ac0388"},{"image":"ghcr.io/github/github-mcp-server:v1.0.4"},{"image":"node:lts-alpine","digest":"sha256:d1b3b4da11eefd5941e7f0b9cf17783fc99d9c6fc34884a665f40a06dbdfc94f","pinned_image":"node:lts-alpine@sha256:d1b3b4da11eefd5941e7f0b9cf17783fc99d9c6fc34884a665f40a06dbdfc94f"}]}
|
||||
# ___ _ _
|
||||
# / _ \ | | (_)
|
||||
@@ -38,7 +38,7 @@
|
||||
# - actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 (source v9)
|
||||
# - actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
|
||||
# - actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
|
||||
# - github/gh-aw-actions/setup@b11be78086764c43fa463398aed7ffdcf40549c1 # v0.77.0
|
||||
# - github/gh-aw-actions/setup@318d7f4901f78b85e25b91709cf0109ac9b425f6 # v0.74.9
|
||||
#
|
||||
# Container images used:
|
||||
# - ghcr.io/github/gh-aw-firewall/agent:0.25.49
|
||||
@@ -52,6 +52,8 @@ name: "Add Community Preset from Issue Submission"
|
||||
on:
|
||||
issues:
|
||||
types:
|
||||
- opened
|
||||
- edited
|
||||
- labeled
|
||||
# skip-bots: # Skip-bots processed as bot check in pre-activation job
|
||||
# - github-actions # Skip-bots processed as bot check in pre-activation job
|
||||
@@ -90,7 +92,7 @@ jobs:
|
||||
steps:
|
||||
- name: Setup Scripts
|
||||
id: setup
|
||||
uses: github/gh-aw-actions/setup@b11be78086764c43fa463398aed7ffdcf40549c1 # v0.77.0
|
||||
uses: github/gh-aw-actions/setup@318d7f4901f78b85e25b91709cf0109ac9b425f6 # v0.74.9
|
||||
with:
|
||||
destination: ${{ runner.temp }}/gh-aw/actions
|
||||
job-name: ${{ github.job }}
|
||||
@@ -204,23 +206,23 @@ jobs:
|
||||
run: |
|
||||
bash "${RUNNER_TEMP}/gh-aw/actions/create_prompt_first.sh"
|
||||
{
|
||||
cat << 'GH_AW_PROMPT_26e9904027e0c5a2_EOF'
|
||||
cat << 'GH_AW_PROMPT_fc7609016a7d28af_EOF'
|
||||
<system>
|
||||
GH_AW_PROMPT_26e9904027e0c5a2_EOF
|
||||
GH_AW_PROMPT_fc7609016a7d28af_EOF
|
||||
cat "${RUNNER_TEMP}/gh-aw/prompts/xpia.md"
|
||||
cat "${RUNNER_TEMP}/gh-aw/prompts/temp_folder_prompt.md"
|
||||
cat "${RUNNER_TEMP}/gh-aw/prompts/markdown.md"
|
||||
cat "${RUNNER_TEMP}/gh-aw/prompts/safe_outputs_prompt.md"
|
||||
cat << 'GH_AW_PROMPT_26e9904027e0c5a2_EOF'
|
||||
cat << 'GH_AW_PROMPT_fc7609016a7d28af_EOF'
|
||||
<safe-output-tools>
|
||||
Tools: add_comment(max:2), create_pull_request, add_labels(max:3), missing_tool, missing_data, noop
|
||||
GH_AW_PROMPT_26e9904027e0c5a2_EOF
|
||||
GH_AW_PROMPT_fc7609016a7d28af_EOF
|
||||
cat "${RUNNER_TEMP}/gh-aw/prompts/safe_outputs_create_pull_request.md"
|
||||
cat << 'GH_AW_PROMPT_26e9904027e0c5a2_EOF'
|
||||
cat << 'GH_AW_PROMPT_fc7609016a7d28af_EOF'
|
||||
</safe-output-tools>
|
||||
GH_AW_PROMPT_26e9904027e0c5a2_EOF
|
||||
GH_AW_PROMPT_fc7609016a7d28af_EOF
|
||||
cat "${RUNNER_TEMP}/gh-aw/prompts/mcp_cli_tools_prompt.md"
|
||||
cat << 'GH_AW_PROMPT_26e9904027e0c5a2_EOF'
|
||||
cat << 'GH_AW_PROMPT_fc7609016a7d28af_EOF'
|
||||
<github-context>
|
||||
The following GitHub context information is available for this workflow:
|
||||
{{#if github.actor}}
|
||||
@@ -252,12 +254,12 @@ jobs:
|
||||
- **Note**: If a branch you need is not in the list above and is not listed as an additional fetched ref, it has NOT been checked out. For private repositories you cannot fetch it without proper authentication. If the branch is required and not available, exit with an error and ask the user to add it to the `fetch:` option of the `checkout:` configuration (e.g., `fetch: ["refs/pulls/open/*"]` for all open PR refs, or `fetch: ["main", "feature/my-branch"]` for specific branches).
|
||||
</github-context>
|
||||
|
||||
GH_AW_PROMPT_26e9904027e0c5a2_EOF
|
||||
GH_AW_PROMPT_fc7609016a7d28af_EOF
|
||||
cat "${RUNNER_TEMP}/gh-aw/prompts/github_mcp_tools_with_safeoutputs_prompt.md"
|
||||
cat << 'GH_AW_PROMPT_26e9904027e0c5a2_EOF'
|
||||
cat << 'GH_AW_PROMPT_fc7609016a7d28af_EOF'
|
||||
</system>
|
||||
{{#runtime-import .github/workflows/add-community-preset.md}}
|
||||
GH_AW_PROMPT_26e9904027e0c5a2_EOF
|
||||
GH_AW_PROMPT_fc7609016a7d28af_EOF
|
||||
} > "$GH_AW_PROMPT"
|
||||
- name: Interpolate variables and render templates
|
||||
uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
|
||||
@@ -368,7 +370,7 @@ jobs:
|
||||
steps:
|
||||
- name: Setup Scripts
|
||||
id: setup
|
||||
uses: github/gh-aw-actions/setup@b11be78086764c43fa463398aed7ffdcf40549c1 # v0.77.0
|
||||
uses: github/gh-aw-actions/setup@318d7f4901f78b85e25b91709cf0109ac9b425f6 # v0.74.9
|
||||
with:
|
||||
destination: ${{ runner.temp }}/gh-aw/actions
|
||||
job-name: ${{ github.job }}
|
||||
@@ -464,9 +466,9 @@ jobs:
|
||||
mkdir -p "${RUNNER_TEMP}/gh-aw/safeoutputs"
|
||||
mkdir -p /tmp/gh-aw/safeoutputs
|
||||
mkdir -p /tmp/gh-aw/mcp-logs/safeoutputs
|
||||
cat > "${RUNNER_TEMP}/gh-aw/safeoutputs/config.json" << 'GH_AW_SAFE_OUTPUTS_CONFIG_36855fee66c4c038_EOF'
|
||||
cat > "${RUNNER_TEMP}/gh-aw/safeoutputs/config.json" << 'GH_AW_SAFE_OUTPUTS_CONFIG_9e8dea0461236832_EOF'
|
||||
{"add_comment":{"max":2},"add_labels":{"allowed":["preset-submission","validation-passed","validation-failed","needs-info"],"max":3},"create_pull_request":{"draft":true,"labels":["preset-submission","automated"],"max":1,"max_patch_files":100,"max_patch_size":1024,"protect_top_level_dot_folders":true,"protected_files":["package.json","bun.lockb","bunfig.toml","deno.json","deno.jsonc","deno.lock","global.json","NuGet.Config","Directory.Packages.props","mix.exs","mix.lock","go.mod","go.sum","stack.yaml","stack.yaml.lock","pom.xml","build.gradle","build.gradle.kts","settings.gradle","settings.gradle.kts","gradle.properties","package-lock.json","yarn.lock","pnpm-lock.yaml","npm-shrinkwrap.json","requirements.txt","Pipfile","Pipfile.lock","pyproject.toml","setup.py","setup.cfg","Gemfile","Gemfile.lock","uv.lock","CODEOWNERS","DESIGN.md","CONTRIBUTING.md","SECURITY.md","CODE_OF_CONDUCT.md","AGENTS.md","CLAUDE.md","GEMINI.md"],"protected_files_policy":"blocked","title_prefix":"[preset] "},"create_report_incomplete_issue":{},"missing_data":{},"missing_tool":{},"noop":{"max":1,"report-as-issue":"true"},"report_incomplete":{}}
|
||||
GH_AW_SAFE_OUTPUTS_CONFIG_36855fee66c4c038_EOF
|
||||
GH_AW_SAFE_OUTPUTS_CONFIG_9e8dea0461236832_EOF
|
||||
- name: Generate Safe Outputs Tools
|
||||
env:
|
||||
GH_AW_TOOLS_META_JSON: |
|
||||
@@ -722,7 +724,7 @@ jobs:
|
||||
|
||||
mkdir -p /home/runner/.copilot
|
||||
GH_AW_NODE=$(which node 2>/dev/null || command -v node 2>/dev/null || echo node)
|
||||
cat << GH_AW_MCP_CONFIG_fdc26b942885c376_EOF | "$GH_AW_NODE" "${RUNNER_TEMP}/gh-aw/actions/start_mcp_gateway.cjs"
|
||||
cat << GH_AW_MCP_CONFIG_c8953ff00c8ee9ee_EOF | "$GH_AW_NODE" "${RUNNER_TEMP}/gh-aw/actions/start_mcp_gateway.cjs"
|
||||
{
|
||||
"mcpServers": {
|
||||
"github": {
|
||||
@@ -763,7 +765,7 @@ jobs:
|
||||
"payloadDir": "${MCP_GATEWAY_PAYLOAD_DIR}"
|
||||
}
|
||||
}
|
||||
GH_AW_MCP_CONFIG_fdc26b942885c376_EOF
|
||||
GH_AW_MCP_CONFIG_c8953ff00c8ee9ee_EOF
|
||||
- name: Mount MCP servers as CLIs
|
||||
id: mount-mcp-clis
|
||||
continue-on-error: true
|
||||
@@ -1045,7 +1047,7 @@ jobs:
|
||||
steps:
|
||||
- name: Setup Scripts
|
||||
id: setup
|
||||
uses: github/gh-aw-actions/setup@b11be78086764c43fa463398aed7ffdcf40549c1 # v0.77.0
|
||||
uses: github/gh-aw-actions/setup@318d7f4901f78b85e25b91709cf0109ac9b425f6 # v0.74.9
|
||||
with:
|
||||
destination: ${{ runner.temp }}/gh-aw/actions
|
||||
job-name: ${{ github.job }}
|
||||
@@ -1186,7 +1188,7 @@ jobs:
|
||||
steps:
|
||||
- name: Setup Scripts
|
||||
id: setup
|
||||
uses: github/gh-aw-actions/setup@b11be78086764c43fa463398aed7ffdcf40549c1 # v0.77.0
|
||||
uses: github/gh-aw-actions/setup@318d7f4901f78b85e25b91709cf0109ac9b425f6 # v0.74.9
|
||||
with:
|
||||
destination: ${{ runner.temp }}/gh-aw/actions
|
||||
job-name: ${{ github.job }}
|
||||
@@ -1382,7 +1384,7 @@ jobs:
|
||||
steps:
|
||||
- name: Setup Scripts
|
||||
id: setup
|
||||
uses: github/gh-aw-actions/setup@b11be78086764c43fa463398aed7ffdcf40549c1 # v0.77.0
|
||||
uses: github/gh-aw-actions/setup@318d7f4901f78b85e25b91709cf0109ac9b425f6 # v0.74.9
|
||||
with:
|
||||
destination: ${{ runner.temp }}/gh-aw/actions
|
||||
job-name: ${{ github.job }}
|
||||
@@ -1454,7 +1456,7 @@ jobs:
|
||||
steps:
|
||||
- name: Setup Scripts
|
||||
id: setup
|
||||
uses: github/gh-aw-actions/setup@b11be78086764c43fa463398aed7ffdcf40549c1 # v0.77.0
|
||||
uses: github/gh-aw-actions/setup@318d7f4901f78b85e25b91709cf0109ac9b425f6 # v0.74.9
|
||||
with:
|
||||
destination: ${{ runner.temp }}/gh-aw/actions
|
||||
job-name: ${{ github.job }}
|
||||
|
||||
15
.github/workflows/add-community-preset.md
vendored
15
.github/workflows/add-community-preset.md
vendored
@@ -4,7 +4,7 @@ emoji: "🎨"
|
||||
|
||||
on:
|
||||
issues:
|
||||
types: [labeled]
|
||||
types: [opened, edited, labeled]
|
||||
skip-bots: [github-actions, copilot, dependabot]
|
||||
|
||||
tools:
|
||||
@@ -22,8 +22,6 @@ checkout:
|
||||
fetch-depth: 0
|
||||
|
||||
safe-outputs:
|
||||
noop:
|
||||
report-as-issue: false
|
||||
create-pull-request:
|
||||
title-prefix: "[preset] "
|
||||
labels: [preset-submission, automated]
|
||||
@@ -49,9 +47,14 @@ or update entries in the community preset catalog.
|
||||
|
||||
## Triggering Conditions
|
||||
|
||||
This workflow only triggers when the `preset-submission` label is added to an
|
||||
issue. Before processing, verify that the issue title starts with `[Preset]:`.
|
||||
If it does not, stop without commenting.
|
||||
This workflow triggers on issue events. **Only process the issue if ALL of these
|
||||
conditions are met:**
|
||||
|
||||
1. The issue has the `preset-submission` label
|
||||
2. The issue title starts with `[Preset]:`
|
||||
|
||||
If the issue does not meet these conditions, add a brief comment explaining that
|
||||
this workflow only processes preset submission issues, then stop.
|
||||
|
||||
## Step 1 — Read and Parse the Issue
|
||||
|
||||
|
||||
4
.github/workflows/codeql.yml
vendored
4
.github/workflows/codeql.yml
vendored
@@ -22,11 +22,11 @@ jobs:
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
|
||||
- name: Initialize CodeQL
|
||||
uses: github/codeql-action/init@7211b7c8077ea37d8641b6271f6a365a22a5fbfa # v4
|
||||
uses: github/codeql-action/init@9e0d7b8d25671d64c341c19c0152d693099fb5ba # v4
|
||||
with:
|
||||
languages: ${{ matrix.language }}
|
||||
|
||||
- name: Perform CodeQL Analysis
|
||||
uses: github/codeql-action/analyze@7211b7c8077ea37d8641b6271f6a365a22a5fbfa # v4
|
||||
uses: github/codeql-action/analyze@9e0d7b8d25671d64c341c19c0152d693099fb5ba # v4
|
||||
with:
|
||||
category: "/language:${{ matrix.language }}"
|
||||
|
||||
14
CHANGELOG.md
14
CHANGELOG.md
@@ -2,20 +2,6 @@
|
||||
|
||||
<!-- insert new changelog below this comment -->
|
||||
|
||||
## [0.8.18] - 2026-05-29
|
||||
|
||||
### Changed
|
||||
|
||||
- Add support for SPECKIT_WORKFLOW_RUN_ID override (#2742)
|
||||
- feat: support SPECKIT_INTEGRATION_<KEY>_EXECUTABLE env var (#2743)
|
||||
- chore(deps): bump github/gh-aw-actions from 0.74.8 to 0.77.0 (#2754)
|
||||
- chore(deps): bump github/codeql-action from 4.35.5 to 4.36.0 (#2753)
|
||||
- fix: disable no-op issue reporting for catalog submission workflows (#2748)
|
||||
- Add confirmation prompt for URL-based extension installs (#2745)
|
||||
- fix: restrict community submission workflows to labeled event only (#2741)
|
||||
- feat(integrations): support SPECIFY_<KEY>_EXTRA_ARGS env var for agent subprocess flags (#2596)
|
||||
- chore: release 0.8.17, begin 0.8.18.dev0 development (#2737)
|
||||
|
||||
## [0.8.17] - 2026-05-28
|
||||
|
||||
### Changed
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[project]
|
||||
name = "specify-cli"
|
||||
version = "0.8.18"
|
||||
version = "0.8.17"
|
||||
description = "Specify CLI, part of GitHub Spec Kit. A tool to bootstrap your projects for Spec-Driven Development (SDD)."
|
||||
requires-python = ">=3.11"
|
||||
dependencies = [
|
||||
|
||||
@@ -3003,23 +3003,9 @@ def extension_add(
|
||||
console.print("HTTP is only allowed for localhost URLs.")
|
||||
raise typer.Exit(1)
|
||||
|
||||
# Warn about untrusted sources — default-deny confirmation
|
||||
console.print()
|
||||
console.print(Panel(
|
||||
f"[bold]You are installing an extension from an external URL that is not\n"
|
||||
f"listed in any of your configured extension catalogs.[/bold]\n\n"
|
||||
f"URL: {from_url}\n\n"
|
||||
f"Only install extensions from sources you trust.",
|
||||
title="[bold yellow]⚠ Untrusted Source[/bold yellow]",
|
||||
border_style="yellow",
|
||||
padding=(1, 2),
|
||||
))
|
||||
console.print()
|
||||
confirm = typer.confirm("Continue with installation?", default=False)
|
||||
if not confirm:
|
||||
console.print("Cancelled")
|
||||
raise typer.Exit(0)
|
||||
|
||||
# Warn about untrusted sources
|
||||
console.print("[yellow]Warning:[/yellow] Installing from external URL.")
|
||||
console.print("Only install extensions from sources you trust.\n")
|
||||
console.print(f"Downloading from {from_url}...")
|
||||
|
||||
# Download ZIP to temp location
|
||||
|
||||
@@ -90,7 +90,7 @@ class AgyIntegration(SkillsIntegration):
|
||||
output_json: bool = True,
|
||||
) -> list[str] | None:
|
||||
# agy does not support --model or JSON output; both params are ignored
|
||||
return [self._resolve_executable(), "--print", prompt]
|
||||
return ["agy", "--print", prompt]
|
||||
|
||||
def setup(
|
||||
self,
|
||||
|
||||
@@ -13,9 +13,7 @@ Provides:
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import os
|
||||
import re
|
||||
import shlex
|
||||
import shutil
|
||||
from abc import ABC
|
||||
from dataclasses import dataclass
|
||||
@@ -146,65 +144,6 @@ class IntegrationBase(ABC):
|
||||
"""
|
||||
return None
|
||||
|
||||
def _resolve_executable(self) -> str:
|
||||
"""Return the executable for this integration's CLI tool.
|
||||
|
||||
Checks ``SPECKIT_INTEGRATION_<KEY>_EXECUTABLE`` first, allowing
|
||||
operators to override the binary path without modifying the
|
||||
integration configuration — useful when the tool is installed in
|
||||
a non-standard location or a specific version must be pinned.
|
||||
Hyphens in the integration key are replaced with underscores and
|
||||
the key is uppercased so that, for example, ``kiro-cli`` maps to
|
||||
``SPECKIT_INTEGRATION_KIRO_CLI_EXECUTABLE``.
|
||||
|
||||
Falls back to ``self.key`` when the env var is unset or
|
||||
whitespace-only so existing behaviour is unchanged.
|
||||
|
||||
See issue #2596.
|
||||
"""
|
||||
env_name = (
|
||||
f"SPECKIT_INTEGRATION_{self.key.upper().replace('-', '_')}_EXECUTABLE"
|
||||
)
|
||||
override = os.environ.get(env_name, "").strip()
|
||||
return override if override else self.key
|
||||
|
||||
def _apply_extra_args_env_var(self, args: list[str]) -> None:
|
||||
"""Append `SPECKIT_INTEGRATION_<KEY>_EXTRA_ARGS` env-var value to *args*.
|
||||
|
||||
Operators can inject extra CLI flags into the spawned agent
|
||||
subprocess by setting an env var named for the integration key,
|
||||
e.g. `SPECKIT_INTEGRATION_CLAUDE_EXTRA_ARGS="--dangerously-skip-permissions"`.
|
||||
The `INTEGRATION` segment scopes the variable to this subsystem
|
||||
so it does not collide with other Spec Kit env-var namespaces.
|
||||
Hyphens in the integration key are replaced with underscores
|
||||
and the key is uppercased
|
||||
(e.g. `kiro-cli` → `SPECKIT_INTEGRATION_KIRO_CLI_EXTRA_ARGS`).
|
||||
|
||||
Useful in CI / non-interactive contexts where the spawned agent
|
||||
needs flags that change its prompt-handling behaviour.
|
||||
Default behaviour (env var unset or whitespace-only) is a no-op
|
||||
— *args* is unchanged. Multi-token values are parsed via
|
||||
`shlex.split`.
|
||||
|
||||
See issue #2595.
|
||||
"""
|
||||
env_name = (
|
||||
f"SPECKIT_INTEGRATION_{self.key.upper().replace('-', '_')}_EXTRA_ARGS"
|
||||
)
|
||||
extra = os.environ.get(env_name, "").strip()
|
||||
if not extra:
|
||||
return
|
||||
try:
|
||||
tokens = shlex.split(extra)
|
||||
except ValueError as exc:
|
||||
raise ValueError(
|
||||
f"{env_name} is not parseable as a POSIX-quoted command line "
|
||||
f"(value: {extra!r}). shlex reported: {exc}. "
|
||||
f"Use single or double quotes to group multi-word values, e.g. "
|
||||
f'{env_name}=\'--flag "value with spaces"\'.'
|
||||
) from exc
|
||||
args.extend(tokens)
|
||||
|
||||
def build_command_invocation(self, command_name: str, args: str = "") -> str:
|
||||
"""Build the native slash-command invocation for a Spec Kit command.
|
||||
|
||||
@@ -917,8 +856,7 @@ class MarkdownIntegration(IntegrationBase):
|
||||
) -> list[str] | None:
|
||||
if not self.config or not self.config.get("requires_cli"):
|
||||
return None
|
||||
args = [self._resolve_executable(), "-p", prompt]
|
||||
self._apply_extra_args_env_var(args)
|
||||
args = [self.key, "-p", prompt]
|
||||
if model:
|
||||
args.extend(["--model", model])
|
||||
if output_json:
|
||||
@@ -1005,8 +943,7 @@ class TomlIntegration(IntegrationBase):
|
||||
) -> list[str] | None:
|
||||
if not self.config or not self.config.get("requires_cli"):
|
||||
return None
|
||||
args = [self._resolve_executable(), "-p", prompt]
|
||||
self._apply_extra_args_env_var(args)
|
||||
args = [self.key, "-p", prompt]
|
||||
if model:
|
||||
args.extend(["-m", model])
|
||||
if output_json:
|
||||
@@ -1424,8 +1361,7 @@ class SkillsIntegration(IntegrationBase):
|
||||
) -> list[str] | None:
|
||||
if not self.config or not self.config.get("requires_cli"):
|
||||
return None
|
||||
args = [self._resolve_executable(), "-p", prompt]
|
||||
self._apply_extra_args_env_var(args)
|
||||
args = [self.key, "-p", prompt]
|
||||
if model:
|
||||
args.extend(["--model", model])
|
||||
if output_json:
|
||||
|
||||
@@ -37,10 +37,7 @@ class CodexIntegration(SkillsIntegration):
|
||||
output_json: bool = True,
|
||||
) -> list[str] | None:
|
||||
# Codex uses ``codex exec "prompt"`` for non-interactive mode.
|
||||
# Resolve argv[0] via the shared executable resolver so operators can
|
||||
# override the binary with SPECKIT_INTEGRATION_CODEX_EXECUTABLE.
|
||||
args: list[str] = [self._resolve_executable(), "exec", prompt]
|
||||
self._apply_extra_args_env_var(args)
|
||||
args: list[str] = ["codex", "exec", prompt]
|
||||
if model:
|
||||
args.extend(["--model", model])
|
||||
if output_json:
|
||||
|
||||
@@ -134,18 +134,6 @@ class CopilotIntegration(IntegrationBase):
|
||||
),
|
||||
]
|
||||
|
||||
def _resolve_executable(self) -> str:
|
||||
"""Return the Copilot CLI executable, respecting the env-var override.
|
||||
|
||||
Checks ``SPECKIT_INTEGRATION_COPILOT_EXECUTABLE`` first. Falls
|
||||
back to the platform-specific default from ``_copilot_executable()``
|
||||
(``copilot.cmd`` on Windows, ``copilot`` elsewhere) so that
|
||||
existing behaviour is preserved when the env var is unset.
|
||||
"""
|
||||
env_name = "SPECKIT_INTEGRATION_COPILOT_EXECUTABLE"
|
||||
override = os.environ.get(env_name, "").strip()
|
||||
return override if override else _copilot_executable()
|
||||
|
||||
def build_exec_args(
|
||||
self,
|
||||
prompt: str,
|
||||
@@ -160,8 +148,7 @@ class CopilotIntegration(IntegrationBase):
|
||||
# Controlled by SPECKIT_COPILOT_ALLOW_ALL_TOOLS env var
|
||||
# (default: enabled). The deprecated SPECKIT_ALLOW_ALL_TOOLS
|
||||
# is also honoured as a fallback.
|
||||
args = [self._resolve_executable(), "-p", prompt]
|
||||
self._apply_extra_args_env_var(args)
|
||||
args = [_copilot_executable(), "-p", prompt]
|
||||
if _allow_all():
|
||||
args.append("--yolo")
|
||||
if model:
|
||||
@@ -229,12 +216,7 @@ class CopilotIntegration(IntegrationBase):
|
||||
agent_name = f"speckit.{stem}"
|
||||
prompt = args or ""
|
||||
|
||||
cli_args = [self._resolve_executable(), "-p", prompt]
|
||||
# Honour SPECKIT_INTEGRATION_COPILOT_EXTRA_ARGS for real workflow
|
||||
# runs. `dispatch_command` builds cli_args inline rather than
|
||||
# going through `build_exec_args`, so the hook must be invoked
|
||||
# here too — otherwise the env var is silently ignored.
|
||||
self._apply_extra_args_env_var(cli_args)
|
||||
cli_args = [_copilot_executable(), "-p", prompt]
|
||||
if not skills_mode:
|
||||
cli_args.extend(["--agent", agent_name])
|
||||
if _allow_all():
|
||||
|
||||
@@ -48,8 +48,7 @@ class DevinIntegration(SkillsIntegration):
|
||||
stdout instead of structured JSON. ``requires_cli=True`` is
|
||||
kept on the integration for tool detection.
|
||||
"""
|
||||
args = [self._resolve_executable(), "-p", prompt]
|
||||
self._apply_extra_args_env_var(args)
|
||||
args = [self.key, "-p", prompt]
|
||||
if model:
|
||||
args.extend(["--model", model])
|
||||
return args
|
||||
|
||||
@@ -257,7 +257,7 @@ class HermesIntegration(SkillsIntegration):
|
||||
mapping slash-command invocations to the appropriate skill-based
|
||||
dispatch.
|
||||
"""
|
||||
args = [self._resolve_executable(), "chat", "-Q"]
|
||||
args = [self.key, "chat", "-Q"]
|
||||
|
||||
if model:
|
||||
args.extend(["-m", model])
|
||||
|
||||
@@ -28,12 +28,7 @@ class OpencodeIntegration(MarkdownIntegration):
|
||||
model: str | None = None,
|
||||
output_json: bool = True,
|
||||
) -> list[str] | None:
|
||||
args = [self._resolve_executable(), "run"]
|
||||
# Apply operator-injected extra args before the prompt-derived
|
||||
# --command and the canonical --format/-m flags so Spec Kit's
|
||||
# later appends remain authoritative under repeated-flag CLI
|
||||
# semantics.
|
||||
self._apply_extra_args_env_var(args)
|
||||
args = [self.key, "run"]
|
||||
|
||||
message = prompt
|
||||
if prompt.startswith("/"):
|
||||
|
||||
@@ -11,7 +11,6 @@ The engine is the orchestrator that:
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import os
|
||||
import re
|
||||
import uuid
|
||||
from datetime import datetime, timezone
|
||||
@@ -426,7 +425,7 @@ class WorkflowEngine:
|
||||
inputs:
|
||||
User-provided input values.
|
||||
run_id:
|
||||
Optional run ID (uses SPECKIT_WORKFLOW_RUN_ID when set, otherwise auto-generated).
|
||||
Optional run ID (auto-generated if not provided).
|
||||
|
||||
Returns
|
||||
-------
|
||||
@@ -434,14 +433,8 @@ class WorkflowEngine:
|
||||
"""
|
||||
from . import STEP_REGISTRY
|
||||
|
||||
effective_run_id = run_id
|
||||
if effective_run_id is None:
|
||||
env_run_id = os.environ.get("SPECKIT_WORKFLOW_RUN_ID", "").strip()
|
||||
if env_run_id:
|
||||
effective_run_id = env_run_id
|
||||
|
||||
state = RunState(
|
||||
run_id=effective_run_id,
|
||||
run_id=run_id,
|
||||
workflow_id=definition.id,
|
||||
project_root=self.project_root,
|
||||
)
|
||||
|
||||
@@ -1,638 +0,0 @@
|
||||
"""Tests for the per-integration `SPECKIT_INTEGRATION_<KEY>_EXTRA_ARGS` and
|
||||
`SPECKIT_INTEGRATION_<KEY>_EXECUTABLE` env-var hooks.
|
||||
|
||||
The hooks are implemented in `IntegrationBase._apply_extra_args_env_var` and
|
||||
`IntegrationBase._resolve_executable` and wired into every concrete
|
||||
`build_exec_args` — `MarkdownIntegration`, `TomlIntegration`,
|
||||
`SkillsIntegration`, plus override integrations.
|
||||
These tests cover both the shared mechanisms (via `SkillsIntegration` stubs
|
||||
near the top of the file) and override integrations end-to-end (further down).
|
||||
See issues #2595 and #2596."""
|
||||
|
||||
import os
|
||||
|
||||
import pytest
|
||||
|
||||
from specify_cli.integrations.base import (
|
||||
MarkdownIntegration,
|
||||
SkillsIntegration,
|
||||
TomlIntegration,
|
||||
)
|
||||
|
||||
|
||||
class _ClaudeStub(SkillsIntegration):
|
||||
"""Minimal Claude-like SkillsIntegration for testing."""
|
||||
|
||||
key = "claude"
|
||||
config = {
|
||||
"name": "Claude (test stub)",
|
||||
"folder": ".claude/",
|
||||
"commands_subdir": "skills",
|
||||
"install_url": None,
|
||||
"requires_cli": True,
|
||||
}
|
||||
registrar_config = {
|
||||
"dir": ".claude/skills",
|
||||
"format": "markdown",
|
||||
"args": "$ARGUMENTS",
|
||||
"extension": "/SKILL.md",
|
||||
}
|
||||
context_file = "CLAUDE.md"
|
||||
|
||||
|
||||
class _KiroCliStub(SkillsIntegration):
|
||||
"""SkillsIntegration with a hyphenated key to exercise key
|
||||
normalization (`kiro-cli` → `KIRO_CLI`)."""
|
||||
|
||||
key = "kiro-cli"
|
||||
config = {
|
||||
"name": "Kiro CLI (test stub)",
|
||||
"folder": ".kiro/",
|
||||
"commands_subdir": "commands",
|
||||
"install_url": None,
|
||||
"requires_cli": True,
|
||||
}
|
||||
registrar_config = {
|
||||
"dir": ".kiro/commands",
|
||||
"format": "markdown",
|
||||
"args": "$ARGUMENTS",
|
||||
"extension": ".md",
|
||||
}
|
||||
context_file = "KIRO.md"
|
||||
|
||||
|
||||
class _NoCliStub(SkillsIntegration):
|
||||
"""SkillsIntegration with requires_cli=False — build_exec_args
|
||||
must return None and the env-var hook must not fire."""
|
||||
|
||||
key = "no-cli"
|
||||
config = {
|
||||
"name": "No-CLI agent (test stub)",
|
||||
"folder": ".no-cli/",
|
||||
"commands_subdir": "commands",
|
||||
"install_url": None,
|
||||
"requires_cli": False,
|
||||
}
|
||||
registrar_config = {
|
||||
"dir": ".no-cli/commands",
|
||||
"format": "markdown",
|
||||
"args": "$ARGUMENTS",
|
||||
"extension": ".md",
|
||||
}
|
||||
context_file = "NOCLI.md"
|
||||
|
||||
|
||||
class _MarkdownAgentStub(MarkdownIntegration):
|
||||
"""Bare MarkdownIntegration subclass — does NOT override
|
||||
`build_exec_args`. Locks the base implementation in
|
||||
`MarkdownIntegration.build_exec_args` for the common case
|
||||
(most concrete integrations: Amp, Auggie, Generic, …)."""
|
||||
|
||||
key = "md-agent"
|
||||
config = {
|
||||
"name": "Markdown agent (test stub)",
|
||||
"folder": ".md-agent/",
|
||||
"commands_subdir": "commands",
|
||||
"install_url": None,
|
||||
"requires_cli": True,
|
||||
}
|
||||
registrar_config = {
|
||||
"dir": ".md-agent/commands",
|
||||
"format": "markdown",
|
||||
"args": "$ARGUMENTS",
|
||||
"extension": ".md",
|
||||
}
|
||||
context_file = "MDAGENT.md"
|
||||
|
||||
|
||||
class _TomlAgentStub(TomlIntegration):
|
||||
"""Bare TomlIntegration subclass — does NOT override
|
||||
`build_exec_args`. Locks the base implementation in
|
||||
`TomlIntegration.build_exec_args` (Gemini, Tabnine)."""
|
||||
|
||||
key = "toml-agent"
|
||||
config = {
|
||||
"name": "TOML agent (test stub)",
|
||||
"folder": ".toml-agent/",
|
||||
"commands_subdir": "commands",
|
||||
"install_url": None,
|
||||
"requires_cli": True,
|
||||
}
|
||||
registrar_config = {
|
||||
"dir": ".toml-agent/commands",
|
||||
"format": "toml",
|
||||
"args": "$ARGUMENTS",
|
||||
"extension": ".toml",
|
||||
}
|
||||
context_file = "TOMLAGENT.md"
|
||||
|
||||
|
||||
@pytest.fixture(autouse=True)
|
||||
def _clean_extra_args_env(monkeypatch):
|
||||
"""Strip any leaked SPECKIT_INTEGRATION_*_EXTRA_ARGS and
|
||||
SPECKIT_INTEGRATION_*_EXECUTABLE vars from the test env so a
|
||||
developer's shell setting doesn't pollute results."""
|
||||
for key in list(os.environ):
|
||||
if key.startswith("SPECKIT_INTEGRATION_") and (
|
||||
key.endswith("_EXTRA_ARGS") or key.endswith("_EXECUTABLE")
|
||||
):
|
||||
monkeypatch.delenv(key, raising=False)
|
||||
|
||||
|
||||
def test_env_var_unset_byte_identical_argv():
|
||||
"""Default behaviour: env var unset → no extra args inserted.
|
||||
|
||||
Locks the backward-compatibility guarantee that existing
|
||||
operators see no change.
|
||||
"""
|
||||
args = _ClaudeStub().build_exec_args("hello prompt")
|
||||
assert args == ["claude", "-p", "hello prompt", "--output-format", "json"]
|
||||
|
||||
|
||||
def test_env_var_set_flag_inserted_before_model_and_output_format(
|
||||
monkeypatch,
|
||||
):
|
||||
monkeypatch.setenv(
|
||||
"SPECKIT_INTEGRATION_CLAUDE_EXTRA_ARGS", "--dangerously-skip-permissions"
|
||||
)
|
||||
args = _ClaudeStub().build_exec_args("hello prompt", model="sonnet")
|
||||
assert args == [
|
||||
"claude",
|
||||
"-p",
|
||||
"hello prompt",
|
||||
"--dangerously-skip-permissions",
|
||||
"--model",
|
||||
"sonnet",
|
||||
"--output-format",
|
||||
"json",
|
||||
]
|
||||
|
||||
|
||||
def test_env_var_multi_token_parsed_via_shlex(monkeypatch):
|
||||
monkeypatch.setenv(
|
||||
"SPECKIT_INTEGRATION_CLAUDE_EXTRA_ARGS",
|
||||
"--dangerously-skip-permissions --max-turns 3",
|
||||
)
|
||||
args = _ClaudeStub().build_exec_args("p")
|
||||
assert args == [
|
||||
"claude",
|
||||
"-p",
|
||||
"p",
|
||||
"--dangerously-skip-permissions",
|
||||
"--max-turns",
|
||||
"3",
|
||||
"--output-format",
|
||||
"json",
|
||||
]
|
||||
|
||||
|
||||
def test_malformed_quoting_raises_actionable_value_error(monkeypatch):
|
||||
"""An unmatched quote in the env-var value must surface a clear
|
||||
error naming the offending env var and showing the invalid value,
|
||||
rather than crashing workflow dispatch with a bare shlex traceback."""
|
||||
monkeypatch.setenv(
|
||||
"SPECKIT_INTEGRATION_CLAUDE_EXTRA_ARGS",
|
||||
'--flag "unterminated',
|
||||
)
|
||||
with pytest.raises(ValueError) as excinfo:
|
||||
_ClaudeStub().build_exec_args("p")
|
||||
msg = str(excinfo.value)
|
||||
assert "SPECKIT_INTEGRATION_CLAUDE_EXTRA_ARGS" in msg
|
||||
assert "--flag \"unterminated" in msg
|
||||
|
||||
|
||||
def test_env_var_empty_or_whitespace_is_noop(monkeypatch):
|
||||
"""An env var set to '' or ' ' is treated as unset."""
|
||||
monkeypatch.setenv("SPECKIT_INTEGRATION_CLAUDE_EXTRA_ARGS", " ")
|
||||
args = _ClaudeStub().build_exec_args("p")
|
||||
assert args == ["claude", "-p", "p", "--output-format", "json"]
|
||||
|
||||
|
||||
def test_other_integration_env_var_ignored(monkeypatch):
|
||||
"""`SPECKIT_INTEGRATION_GEMINI_EXTRA_ARGS` set must NOT leak into
|
||||
Claude's argv (per-integration scoping)."""
|
||||
monkeypatch.setenv("SPECKIT_INTEGRATION_GEMINI_EXTRA_ARGS", "--gemini-only-flag")
|
||||
args = _ClaudeStub().build_exec_args("p")
|
||||
assert args == ["claude", "-p", "p", "--output-format", "json"]
|
||||
|
||||
|
||||
def test_key_normalization_hyphen_to_underscore_uppercase(monkeypatch):
|
||||
"""`kiro-cli` key looks up `SPECKIT_INTEGRATION_KIRO_CLI_EXTRA_ARGS`
|
||||
(hyphens replaced with underscores, then uppercased)."""
|
||||
monkeypatch.setenv(
|
||||
"SPECKIT_INTEGRATION_KIRO_CLI_EXTRA_ARGS", "--some-kiro-flag"
|
||||
)
|
||||
args = _KiroCliStub().build_exec_args("p")
|
||||
assert args == [
|
||||
"kiro-cli",
|
||||
"-p",
|
||||
"p",
|
||||
"--some-kiro-flag",
|
||||
"--output-format",
|
||||
"json",
|
||||
]
|
||||
|
||||
|
||||
def test_requires_cli_false_returns_none(monkeypatch):
|
||||
"""`requires_cli: False` short-circuits to None — the env-var
|
||||
hook is never reached and no argv is built."""
|
||||
monkeypatch.setenv("SPECKIT_INTEGRATION_NO_CLI_EXTRA_ARGS", "--should-not-appear")
|
||||
assert _NoCliStub().build_exec_args("p") is None
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Base-class coverage
|
||||
#
|
||||
# Most integrations inherit `build_exec_args` from `MarkdownIntegration`
|
||||
# or `TomlIntegration` without overriding it. The tests above use
|
||||
# `SkillsIntegration` stubs (which share the same hook mechanism) — these
|
||||
# tests exercise the two other base implementations directly so all three
|
||||
# concrete bases are covered.
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
def test_markdown_integration_base_honours_extra_args(monkeypatch):
|
||||
"""A bare `MarkdownIntegration` subclass — which does not override
|
||||
`build_exec_args` — must honour the env var via the base
|
||||
implementation. Covers the most common integration pattern."""
|
||||
monkeypatch.setenv(
|
||||
"SPECKIT_INTEGRATION_MD_AGENT_EXTRA_ARGS", "--debug --max-tokens 100"
|
||||
)
|
||||
args = _MarkdownAgentStub().build_exec_args("p")
|
||||
assert args == [
|
||||
"md-agent",
|
||||
"-p",
|
||||
"p",
|
||||
"--debug",
|
||||
"--max-tokens",
|
||||
"100",
|
||||
"--output-format",
|
||||
"json",
|
||||
]
|
||||
|
||||
|
||||
def test_toml_integration_base_honours_extra_args(monkeypatch):
|
||||
"""A bare `TomlIntegration` subclass — which does not override
|
||||
`build_exec_args` — must honour the env var via the base
|
||||
implementation. Covers Gemini/Tabnine-style integrations."""
|
||||
monkeypatch.setenv(
|
||||
"SPECKIT_INTEGRATION_TOML_AGENT_EXTRA_ARGS", "--yolo"
|
||||
)
|
||||
args = _TomlAgentStub().build_exec_args("p", model="gemini-pro")
|
||||
# TomlIntegration uses `-m` for model (vs Markdown's `--model`).
|
||||
assert args == [
|
||||
"toml-agent",
|
||||
"-p",
|
||||
"p",
|
||||
"--yolo",
|
||||
"-m",
|
||||
"gemini-pro",
|
||||
"--output-format",
|
||||
"json",
|
||||
]
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Override-integration coverage
|
||||
#
|
||||
# CodexIntegration, DevinIntegration, OpencodeIntegration and
|
||||
# CopilotIntegration each override `build_exec_args` rather than using the
|
||||
# base implementations. The env-var hook must be wired into every override
|
||||
# so the documented behaviour ("works for every requires_cli integration")
|
||||
# is honoured. These tests lock that contract per integration.
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
def test_codex_integration_honours_extra_args(monkeypatch):
|
||||
from specify_cli.integrations.codex import CodexIntegration
|
||||
|
||||
monkeypatch.setenv("SPECKIT_INTEGRATION_CODEX_EXTRA_ARGS", "--sandbox read-only")
|
||||
args = CodexIntegration().build_exec_args("p", model="gpt-5")
|
||||
assert args == [
|
||||
"codex",
|
||||
"exec",
|
||||
"p",
|
||||
"--sandbox",
|
||||
"read-only",
|
||||
"--model",
|
||||
"gpt-5",
|
||||
"--json",
|
||||
]
|
||||
|
||||
|
||||
def test_devin_integration_honours_extra_args(monkeypatch):
|
||||
from specify_cli.integrations.devin import DevinIntegration
|
||||
|
||||
monkeypatch.setenv("SPECKIT_INTEGRATION_DEVIN_EXTRA_ARGS", "--no-confirm")
|
||||
args = DevinIntegration().build_exec_args("p")
|
||||
assert args == ["devin", "-p", "p", "--no-confirm"]
|
||||
|
||||
|
||||
def test_opencode_integration_honours_extra_args(monkeypatch):
|
||||
from specify_cli.integrations.opencode import OpencodeIntegration
|
||||
|
||||
monkeypatch.setenv("SPECKIT_INTEGRATION_OPENCODE_EXTRA_ARGS", "--quiet")
|
||||
args = OpencodeIntegration().build_exec_args("p")
|
||||
assert args == [
|
||||
"opencode",
|
||||
"run",
|
||||
"--quiet",
|
||||
"--format",
|
||||
"json",
|
||||
"p",
|
||||
]
|
||||
|
||||
|
||||
def test_opencode_extra_args_cannot_clobber_prompt_derived_command(
|
||||
monkeypatch,
|
||||
):
|
||||
"""Operator-injected extra args must appear BEFORE the prompt-derived
|
||||
``--command <X>`` so that Spec Kit's command selection wins under
|
||||
repeated-flag CLI semantics (last value typically takes precedence).
|
||||
|
||||
Locks against the regression where an operator setting
|
||||
``SPECKIT_INTEGRATION_OPENCODE_EXTRA_ARGS="--command malicious"`` could redirect
|
||||
a slash-prefixed prompt to a different command.
|
||||
"""
|
||||
from specify_cli.integrations.opencode import OpencodeIntegration
|
||||
|
||||
monkeypatch.setenv(
|
||||
"SPECKIT_INTEGRATION_OPENCODE_EXTRA_ARGS", "--command operator-override"
|
||||
)
|
||||
args = OpencodeIntegration().build_exec_args("/speckit body text")
|
||||
# Prompt-derived "--command speckit" appears AFTER the
|
||||
# operator-injected one, so a CLI that resolves repeated flags
|
||||
# last-wins will honour Spec Kit's choice.
|
||||
assert args == [
|
||||
"opencode",
|
||||
"run",
|
||||
"--command",
|
||||
"operator-override",
|
||||
"--command",
|
||||
"speckit",
|
||||
"--format",
|
||||
"json",
|
||||
"body text",
|
||||
]
|
||||
|
||||
|
||||
def test_copilot_integration_honours_extra_args(monkeypatch):
|
||||
from specify_cli.integrations.copilot import (
|
||||
CopilotIntegration,
|
||||
_copilot_executable,
|
||||
)
|
||||
|
||||
# Disable --yolo so the argv shape stays deterministic.
|
||||
monkeypatch.setenv("SPECKIT_COPILOT_ALLOW_ALL_TOOLS", "0")
|
||||
monkeypatch.setenv(
|
||||
"SPECKIT_INTEGRATION_COPILOT_EXTRA_ARGS", "--allow-tool 'shell(echo)'"
|
||||
)
|
||||
args = CopilotIntegration().build_exec_args("p")
|
||||
# `_copilot_executable()` returns "copilot.cmd" on Windows and
|
||||
# "copilot" elsewhere; the test must mirror that to stay portable.
|
||||
assert args == [
|
||||
_copilot_executable(),
|
||||
"-p",
|
||||
"p",
|
||||
"--allow-tool",
|
||||
"shell(echo)",
|
||||
"--output-format",
|
||||
"json",
|
||||
]
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# `dispatch_command` end-to-end coverage
|
||||
#
|
||||
# Workflow execution calls `impl.dispatch_command(...)`, not
|
||||
# `build_exec_args` directly. `IntegrationBase.dispatch_command` delegates
|
||||
# to `build_exec_args` (so the override fixes above flow through), but
|
||||
# `CopilotIntegration` overrides `dispatch_command` and constructs
|
||||
# `cli_args` inline — the hook must be invoked there too or the env var
|
||||
# is silently ignored at workflow runtime. These tests monkeypatch
|
||||
# `subprocess.run` and assert the env-var args reach the executed argv.
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
class _RunCapture:
|
||||
"""Test double that captures argv passed to subprocess.run."""
|
||||
|
||||
def __init__(self):
|
||||
self.captured_args: list[str] | None = None
|
||||
|
||||
def __call__(self, args, **kwargs):
|
||||
self.captured_args = list(args)
|
||||
|
||||
class _Result:
|
||||
returncode = 0
|
||||
stdout = ""
|
||||
stderr = ""
|
||||
|
||||
return _Result()
|
||||
|
||||
|
||||
def test_copilot_dispatch_command_includes_extra_args(monkeypatch):
|
||||
"""Locks the bypass fix: `CopilotIntegration.dispatch_command`
|
||||
must honour `SPECKIT_INTEGRATION_COPILOT_EXTRA_ARGS`, not just `build_exec_args`.
|
||||
"""
|
||||
import subprocess
|
||||
|
||||
from specify_cli.integrations.copilot import CopilotIntegration
|
||||
|
||||
capture = _RunCapture()
|
||||
monkeypatch.setattr(subprocess, "run", capture)
|
||||
monkeypatch.setenv("SPECKIT_COPILOT_ALLOW_ALL_TOOLS", "0")
|
||||
monkeypatch.setenv(
|
||||
"SPECKIT_INTEGRATION_COPILOT_EXTRA_ARGS", "--allow-tool 'shell(echo)'"
|
||||
)
|
||||
|
||||
CopilotIntegration().dispatch_command(
|
||||
"speckit.plan", args="body", stream=False
|
||||
)
|
||||
|
||||
assert capture.captured_args is not None
|
||||
# Hook inserted between `-p prompt` and the canonical Copilot flags.
|
||||
p_idx = capture.captured_args.index("-p")
|
||||
agent_idx = capture.captured_args.index("--agent")
|
||||
extra_idx = capture.captured_args.index("--allow-tool")
|
||||
assert p_idx < extra_idx < agent_idx
|
||||
assert "shell(echo)" in capture.captured_args
|
||||
|
||||
|
||||
def test_codex_dispatch_command_includes_extra_args(monkeypatch):
|
||||
"""Lock the inherited `IntegrationBase.dispatch_command` path:
|
||||
Codex (and by transitivity Devin, Opencode) flow through
|
||||
`build_exec_args`, so the env var must reach argv at workflow
|
||||
runtime.
|
||||
"""
|
||||
import subprocess
|
||||
|
||||
from specify_cli.integrations.codex import CodexIntegration
|
||||
|
||||
capture = _RunCapture()
|
||||
monkeypatch.setattr(subprocess, "run", capture)
|
||||
monkeypatch.setenv("SPECKIT_INTEGRATION_CODEX_EXTRA_ARGS", "--sandbox read-only")
|
||||
|
||||
CodexIntegration().dispatch_command(
|
||||
"speckit.plan", args="body", stream=False
|
||||
)
|
||||
|
||||
assert capture.captured_args is not None
|
||||
assert "--sandbox" in capture.captured_args
|
||||
assert "read-only" in capture.captured_args
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# SPECKIT_INTEGRATION_<KEY>_EXECUTABLE tests
|
||||
#
|
||||
# The `_resolve_executable()` method on `IntegrationBase` checks
|
||||
# `SPECKIT_INTEGRATION_<KEY>_EXECUTABLE` and, when set, substitutes that
|
||||
# value for `self.key` as the first token in argv. The tests below lock
|
||||
# the behaviour across shared and override integration paths:
|
||||
# - the shared SkillsIntegration/MarkdownIntegration/TomlIntegration bases,
|
||||
# - representative override integrations,
|
||||
# - the hyphen→underscore key normalisation, and
|
||||
# - whitespace/unset no-op guarantee.
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
def test_executable_env_var_unset_uses_key():
|
||||
"""Default: no override → executable is the integration key."""
|
||||
args = _ClaudeStub().build_exec_args("p")
|
||||
assert args[0] == "claude"
|
||||
|
||||
|
||||
def test_executable_env_var_replaces_first_argv_token(monkeypatch):
|
||||
"""Setting the env var substitutes the executable name in argv."""
|
||||
monkeypatch.setenv("SPECKIT_INTEGRATION_CLAUDE_EXECUTABLE", "/opt/claude/bin/claude")
|
||||
args = _ClaudeStub().build_exec_args("hello")
|
||||
assert args[0] == "/opt/claude/bin/claude"
|
||||
assert args[1:] == ["-p", "hello", "--output-format", "json"]
|
||||
|
||||
|
||||
def test_executable_env_var_whitespace_only_falls_back_to_key(monkeypatch):
|
||||
"""Whitespace-only value is treated as unset → falls back to self.key."""
|
||||
monkeypatch.setenv("SPECKIT_INTEGRATION_CLAUDE_EXECUTABLE", " ")
|
||||
args = _ClaudeStub().build_exec_args("p")
|
||||
assert args[0] == "claude"
|
||||
|
||||
|
||||
def test_executable_env_var_key_normalization_hyphen_to_underscore(monkeypatch):
|
||||
"""`kiro-cli` key maps to `SPECKIT_INTEGRATION_KIRO_CLI_EXECUTABLE`."""
|
||||
monkeypatch.setenv("SPECKIT_INTEGRATION_KIRO_CLI_EXECUTABLE", "/usr/local/bin/kiro-cli")
|
||||
args = _KiroCliStub().build_exec_args("p")
|
||||
assert args[0] == "/usr/local/bin/kiro-cli"
|
||||
|
||||
|
||||
def test_executable_env_var_other_integration_ignored(monkeypatch):
|
||||
"""`SPECKIT_INTEGRATION_GEMINI_EXECUTABLE` must NOT affect Claude."""
|
||||
monkeypatch.setenv("SPECKIT_INTEGRATION_GEMINI_EXECUTABLE", "/custom/gemini")
|
||||
args = _ClaudeStub().build_exec_args("p")
|
||||
assert args[0] == "claude"
|
||||
|
||||
|
||||
def test_executable_env_var_markdown_integration(monkeypatch):
|
||||
"""MarkdownIntegration base honours the executable env var."""
|
||||
monkeypatch.setenv("SPECKIT_INTEGRATION_MD_AGENT_EXECUTABLE", "/custom/md-agent")
|
||||
args = _MarkdownAgentStub().build_exec_args("p")
|
||||
assert args[0] == "/custom/md-agent"
|
||||
|
||||
|
||||
def test_executable_env_var_toml_integration(monkeypatch):
|
||||
"""TomlIntegration base honours the executable env var."""
|
||||
monkeypatch.setenv("SPECKIT_INTEGRATION_TOML_AGENT_EXECUTABLE", "/custom/toml-agent")
|
||||
args = _TomlAgentStub().build_exec_args("p")
|
||||
assert args[0] == "/custom/toml-agent"
|
||||
|
||||
|
||||
def test_executable_env_var_requires_cli_false_returns_none(monkeypatch):
|
||||
"""`requires_cli: False` still returns None even when executable is set."""
|
||||
monkeypatch.setenv("SPECKIT_INTEGRATION_NO_CLI_EXECUTABLE", "/custom/no-cli")
|
||||
assert _NoCliStub().build_exec_args("p") is None
|
||||
|
||||
|
||||
def test_executable_env_var_codex_integration(monkeypatch):
|
||||
"""CodexIntegration honours the executable env var."""
|
||||
from specify_cli.integrations.codex import CodexIntegration
|
||||
|
||||
monkeypatch.setenv("SPECKIT_INTEGRATION_CODEX_EXECUTABLE", "/opt/codex")
|
||||
args = CodexIntegration().build_exec_args("p")
|
||||
assert args[0] == "/opt/codex"
|
||||
assert args[1] == "exec"
|
||||
|
||||
|
||||
def test_executable_env_var_devin_integration(monkeypatch):
|
||||
"""DevinIntegration honours the executable env var."""
|
||||
from specify_cli.integrations.devin import DevinIntegration
|
||||
|
||||
monkeypatch.setenv("SPECKIT_INTEGRATION_DEVIN_EXECUTABLE", "/opt/devin")
|
||||
args = DevinIntegration().build_exec_args("p")
|
||||
assert args[0] == "/opt/devin"
|
||||
|
||||
|
||||
def test_executable_env_var_opencode_integration(monkeypatch):
|
||||
"""OpencodeIntegration honours the executable env var."""
|
||||
from specify_cli.integrations.opencode import OpencodeIntegration
|
||||
|
||||
monkeypatch.setenv("SPECKIT_INTEGRATION_OPENCODE_EXECUTABLE", "/opt/opencode")
|
||||
args = OpencodeIntegration().build_exec_args("p")
|
||||
assert args[0] == "/opt/opencode"
|
||||
assert args[1] == "run"
|
||||
|
||||
|
||||
def test_executable_env_var_copilot_integration(monkeypatch):
|
||||
"""CopilotIntegration honours the executable env var, overriding the
|
||||
platform-specific default from `_copilot_executable()`."""
|
||||
from specify_cli.integrations.copilot import CopilotIntegration
|
||||
|
||||
monkeypatch.setenv("SPECKIT_INTEGRATION_COPILOT_EXECUTABLE", "/opt/copilot")
|
||||
monkeypatch.setenv("SPECKIT_COPILOT_ALLOW_ALL_TOOLS", "0")
|
||||
args = CopilotIntegration().build_exec_args("p")
|
||||
assert args[0] == "/opt/copilot"
|
||||
|
||||
|
||||
def test_executable_env_var_copilot_unset_uses_platform_default(monkeypatch):
|
||||
"""When `SPECKIT_INTEGRATION_COPILOT_EXECUTABLE` is unset, Copilot
|
||||
falls back to the platform-specific default from `_copilot_executable()`."""
|
||||
from specify_cli.integrations.copilot import CopilotIntegration, _copilot_executable
|
||||
|
||||
monkeypatch.setenv("SPECKIT_COPILOT_ALLOW_ALL_TOOLS", "0")
|
||||
args = CopilotIntegration().build_exec_args("p")
|
||||
assert args[0] == _copilot_executable()
|
||||
|
||||
|
||||
def test_executable_env_var_copilot_dispatch_command(monkeypatch):
|
||||
"""CopilotIntegration.dispatch_command honours the executable env var."""
|
||||
import subprocess
|
||||
|
||||
from specify_cli.integrations.copilot import CopilotIntegration
|
||||
|
||||
capture = _RunCapture()
|
||||
monkeypatch.setattr(subprocess, "run", capture)
|
||||
monkeypatch.setenv("SPECKIT_INTEGRATION_COPILOT_EXECUTABLE", "/opt/copilot")
|
||||
monkeypatch.setenv("SPECKIT_COPILOT_ALLOW_ALL_TOOLS", "0")
|
||||
|
||||
CopilotIntegration().dispatch_command("speckit.plan", args="body", stream=False)
|
||||
|
||||
assert capture.captured_args is not None
|
||||
assert capture.captured_args[0] == "/opt/copilot"
|
||||
|
||||
|
||||
def test_executable_and_extra_args_both_honoured(monkeypatch):
|
||||
"""Both the executable override and extra args env vars can be set
|
||||
simultaneously — they are independent hooks."""
|
||||
monkeypatch.setenv("SPECKIT_INTEGRATION_CLAUDE_EXECUTABLE", "/opt/claude")
|
||||
monkeypatch.setenv(
|
||||
"SPECKIT_INTEGRATION_CLAUDE_EXTRA_ARGS", "--dangerously-skip-permissions"
|
||||
)
|
||||
args = _ClaudeStub().build_exec_args("hello", model="sonnet")
|
||||
assert args == [
|
||||
"/opt/claude",
|
||||
"-p",
|
||||
"hello",
|
||||
"--dangerously-skip-permissions",
|
||||
"--model",
|
||||
"sonnet",
|
||||
"--output-format",
|
||||
"json",
|
||||
]
|
||||
@@ -2332,48 +2332,6 @@ steps:
|
||||
assert state.status == RunStatus.COMPLETED
|
||||
assert state.step_results["only-step"]["output"]["stdout"].strip() == "hello"
|
||||
|
||||
def test_run_id_uses_speckit_workflow_run_id_env_override(self, project_dir, monkeypatch):
|
||||
"""When no run_id argument is provided, SPECKIT_WORKFLOW_RUN_ID overrides the auto-generated run ID."""
|
||||
from specify_cli.workflows.engine import WorkflowDefinition, WorkflowEngine
|
||||
|
||||
monkeypatch.setenv("SPECKIT_WORKFLOW_RUN_ID", "env-run-123")
|
||||
definition = WorkflowDefinition.from_string("""
|
||||
schema_version: "1.0"
|
||||
workflow:
|
||||
id: "env-run-id"
|
||||
name: "Env Run Id"
|
||||
version: "1.0.0"
|
||||
steps:
|
||||
- id: stamp
|
||||
type: shell
|
||||
run: "echo {{ context.run_id }}"
|
||||
""")
|
||||
state = WorkflowEngine(project_dir).execute(definition)
|
||||
|
||||
assert state.run_id == "env-run-123"
|
||||
assert state.step_results["stamp"]["output"]["stdout"].strip() == "env-run-123"
|
||||
|
||||
def test_run_id_arg_takes_precedence_over_env_override(self, project_dir, monkeypatch):
|
||||
"""Explicit run_id keeps existing precedence over SPECKIT_WORKFLOW_RUN_ID."""
|
||||
from specify_cli.workflows.engine import WorkflowDefinition, WorkflowEngine
|
||||
|
||||
monkeypatch.setenv("SPECKIT_WORKFLOW_RUN_ID", "env-run-123")
|
||||
definition = WorkflowDefinition.from_string("""
|
||||
schema_version: "1.0"
|
||||
workflow:
|
||||
id: "explicit-run-id"
|
||||
name: "Explicit Run Id"
|
||||
version: "1.0.0"
|
||||
steps:
|
||||
- id: stamp
|
||||
type: shell
|
||||
run: "echo {{ context.run_id }}"
|
||||
""")
|
||||
state = WorkflowEngine(project_dir).execute(definition, run_id="explicit-456")
|
||||
|
||||
assert state.run_id == "explicit-456"
|
||||
assert state.step_results["stamp"]["output"]["stdout"].strip() == "explicit-456"
|
||||
|
||||
|
||||
# ===== State Persistence Tests =====
|
||||
|
||||
|
||||
Reference in New Issue
Block a user