diff --git a/.github/workflows/test-version-prompt.lock.yml b/.github/workflows/test-version-prompt.lock.yml new file mode 100644 index 00000000000..d08c6ff6639 --- /dev/null +++ b/.github/workflows/test-version-prompt.lock.yml @@ -0,0 +1,504 @@ +# +# ___ _ _ +# / _ \ | | (_) +# | |_| | __ _ ___ _ __ | |_ _ ___ +# | _ |/ _` |/ _ \ '_ \| __| |/ __| +# | | | | (_| | __/ | | | |_| | (__ +# \_| |_/\__, |\___|_| |_|\__|_|\___| +# __/ | +# _ _ |___/ +# | | | | / _| | +# | | | | ___ _ __ _ __| |_| | _____ ____ +# | |/\| |/ _ \ '__| |/ /| _| |/ _ \ \ /\ / / ___| +# \ /\ / (_) | | | | ( | | | | (_) \ V V /\__ \ +# \/ \/ \___/|_| |_|\_\|_| |_|\___/ \_/\_/ |___/ +# +# This file was automatically generated by gh-aw. DO NOT EDIT. +# +# To update this file, edit the corresponding .md file and run: +# gh aw compile +# Not all edits will cause changes to this file. +# +# For more information: https://github.github.com/gh-aw/introduction/overview/ +# +# Example workflow demonstrating prompt versioning +# +# Prompt Version: 1.0.0 +# +# frontmatter-hash: fc2a1c66840b01735bfca75f2b264891ad8f3be3ac2146c36207669380c887b6 + +name: "Test Versioned Prompt" +"on": + workflow_dispatch: + +permissions: {} + +concurrency: + group: "gh-aw-${{ github.workflow }}" + +run-name: "Test Versioned Prompt" + +jobs: + activation: + runs-on: ubuntu-slim + permissions: + contents: read + outputs: + comment_id: "" + comment_repo: "" + steps: + - name: Checkout actions folder + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + with: + sparse-checkout: | + actions + persist-credentials: false + - name: Setup Scripts + uses: ./actions/setup + with: + destination: /opt/gh-aw/actions + - name: Checkout .github and .agents folders + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + with: + sparse-checkout: | + .github + .agents + fetch-depth: 1 + persist-credentials: false + - name: Check workflow file timestamps + uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8 + env: + GH_AW_WORKFLOW_FILE: "test-version-prompt.lock.yml" + with: + script: | + const { setupGlobals } = require('/opt/gh-aw/actions/setup_globals.cjs'); + setupGlobals(core, github, context, exec, io); + const { main } = require('/opt/gh-aw/actions/check_workflow_timestamp_api.cjs'); + await main(); + - name: Create prompt with built-in context + env: + GH_AW_PROMPT: /tmp/gh-aw/aw-prompts/prompt.txt + GH_AW_GITHUB_ACTOR: ${{ github.actor }} + GH_AW_GITHUB_EVENT_COMMENT_ID: ${{ github.event.comment.id }} + GH_AW_GITHUB_EVENT_DISCUSSION_NUMBER: ${{ github.event.discussion.number }} + GH_AW_GITHUB_EVENT_ISSUE_NUMBER: ${{ github.event.issue.number }} + GH_AW_GITHUB_EVENT_PULL_REQUEST_NUMBER: ${{ github.event.pull_request.number }} + GH_AW_GITHUB_REPOSITORY: ${{ github.repository }} + GH_AW_GITHUB_RUN_ID: ${{ github.run_id }} + GH_AW_GITHUB_WORKSPACE: ${{ github.workspace }} + run: | + bash /opt/gh-aw/actions/create_prompt_first.sh + cat << 'GH_AW_PROMPT_EOF' > "$GH_AW_PROMPT" + + GH_AW_PROMPT_EOF + cat "/opt/gh-aw/prompts/xpia.md" >> "$GH_AW_PROMPT" + cat "/opt/gh-aw/prompts/temp_folder_prompt.md" >> "$GH_AW_PROMPT" + cat "/opt/gh-aw/prompts/markdown.md" >> "$GH_AW_PROMPT" + cat << 'GH_AW_PROMPT_EOF' >> "$GH_AW_PROMPT" + + The following GitHub context information is available for this workflow: + {{#if __GH_AW_GITHUB_ACTOR__ }} + - **actor**: __GH_AW_GITHUB_ACTOR__ + {{/if}} + {{#if __GH_AW_GITHUB_REPOSITORY__ }} + - **repository**: __GH_AW_GITHUB_REPOSITORY__ + {{/if}} + {{#if __GH_AW_GITHUB_WORKSPACE__ }} + - **workspace**: __GH_AW_GITHUB_WORKSPACE__ + {{/if}} + {{#if __GH_AW_GITHUB_EVENT_ISSUE_NUMBER__ }} + - **issue-number**: #__GH_AW_GITHUB_EVENT_ISSUE_NUMBER__ + {{/if}} + {{#if __GH_AW_GITHUB_EVENT_DISCUSSION_NUMBER__ }} + - **discussion-number**: #__GH_AW_GITHUB_EVENT_DISCUSSION_NUMBER__ + {{/if}} + {{#if __GH_AW_GITHUB_EVENT_PULL_REQUEST_NUMBER__ }} + - **pull-request-number**: #__GH_AW_GITHUB_EVENT_PULL_REQUEST_NUMBER__ + {{/if}} + {{#if __GH_AW_GITHUB_EVENT_COMMENT_ID__ }} + - **comment-id**: __GH_AW_GITHUB_EVENT_COMMENT_ID__ + {{/if}} + {{#if __GH_AW_GITHUB_RUN_ID__ }} + - **workflow-run-id**: __GH_AW_GITHUB_RUN_ID__ + {{/if}} + + + GH_AW_PROMPT_EOF + cat << 'GH_AW_PROMPT_EOF' >> "$GH_AW_PROMPT" + + GH_AW_PROMPT_EOF + cat << 'GH_AW_PROMPT_EOF' >> "$GH_AW_PROMPT" + {{#runtime-import .github/workflows/test-version-prompt.md}} + GH_AW_PROMPT_EOF + - name: Interpolate variables and render templates + uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8 + env: + GH_AW_PROMPT: /tmp/gh-aw/aw-prompts/prompt.txt + with: + script: | + const { setupGlobals } = require('/opt/gh-aw/actions/setup_globals.cjs'); + setupGlobals(core, github, context, exec, io); + const { main } = require('/opt/gh-aw/actions/interpolate_prompt.cjs'); + await main(); + - name: Substitute placeholders + uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8 + env: + GH_AW_PROMPT: /tmp/gh-aw/aw-prompts/prompt.txt + GH_AW_GITHUB_ACTOR: ${{ github.actor }} + GH_AW_GITHUB_EVENT_COMMENT_ID: ${{ github.event.comment.id }} + GH_AW_GITHUB_EVENT_DISCUSSION_NUMBER: ${{ github.event.discussion.number }} + GH_AW_GITHUB_EVENT_ISSUE_NUMBER: ${{ github.event.issue.number }} + GH_AW_GITHUB_EVENT_PULL_REQUEST_NUMBER: ${{ github.event.pull_request.number }} + GH_AW_GITHUB_REPOSITORY: ${{ github.repository }} + GH_AW_GITHUB_RUN_ID: ${{ github.run_id }} + GH_AW_GITHUB_WORKSPACE: ${{ github.workspace }} + GH_AW_NEEDS_PRE_ACTIVATION_OUTPUTS_ACTIVATED: ${{ needs.pre_activation.outputs.activated }} + GH_AW_NEEDS_PRE_ACTIVATION_OUTPUTS_MATCHED_COMMAND: ${{ needs.pre_activation.outputs.matched_command }} + with: + script: | + const { setupGlobals } = require('/opt/gh-aw/actions/setup_globals.cjs'); + setupGlobals(core, github, context, exec, io); + + const substitutePlaceholders = require('/opt/gh-aw/actions/substitute_placeholders.cjs'); + + // Call the substitution function + return await substitutePlaceholders({ + file: process.env.GH_AW_PROMPT, + substitutions: { + GH_AW_GITHUB_ACTOR: process.env.GH_AW_GITHUB_ACTOR, + GH_AW_GITHUB_EVENT_COMMENT_ID: process.env.GH_AW_GITHUB_EVENT_COMMENT_ID, + GH_AW_GITHUB_EVENT_DISCUSSION_NUMBER: process.env.GH_AW_GITHUB_EVENT_DISCUSSION_NUMBER, + GH_AW_GITHUB_EVENT_ISSUE_NUMBER: process.env.GH_AW_GITHUB_EVENT_ISSUE_NUMBER, + GH_AW_GITHUB_EVENT_PULL_REQUEST_NUMBER: process.env.GH_AW_GITHUB_EVENT_PULL_REQUEST_NUMBER, + GH_AW_GITHUB_REPOSITORY: process.env.GH_AW_GITHUB_REPOSITORY, + GH_AW_GITHUB_RUN_ID: process.env.GH_AW_GITHUB_RUN_ID, + GH_AW_GITHUB_WORKSPACE: process.env.GH_AW_GITHUB_WORKSPACE, + GH_AW_NEEDS_PRE_ACTIVATION_OUTPUTS_ACTIVATED: process.env.GH_AW_NEEDS_PRE_ACTIVATION_OUTPUTS_ACTIVATED, + GH_AW_NEEDS_PRE_ACTIVATION_OUTPUTS_MATCHED_COMMAND: process.env.GH_AW_NEEDS_PRE_ACTIVATION_OUTPUTS_MATCHED_COMMAND + } + }); + - name: Validate prompt placeholders + env: + GH_AW_PROMPT: /tmp/gh-aw/aw-prompts/prompt.txt + run: bash /opt/gh-aw/actions/validate_prompt_placeholders.sh + - name: Print prompt + env: + GH_AW_PROMPT: /tmp/gh-aw/aw-prompts/prompt.txt + run: bash /opt/gh-aw/actions/print_prompt_summary.sh + - name: Upload prompt artifact + if: success() + uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6 + with: + name: prompt + path: /tmp/gh-aw/aw-prompts/prompt.txt + retention-days: 1 + + agent: + needs: activation + runs-on: ubuntu-latest + permissions: + contents: read + concurrency: + group: "gh-aw-codex-${{ github.workflow }}" + env: + GH_AW_WORKFLOW_ID_SANITIZED: testversionprompt + outputs: + checkout_pr_success: ${{ steps.checkout-pr.outputs.checkout_pr_success || 'true' }} + model: ${{ steps.generate_aw_info.outputs.model }} + secret_verification_result: ${{ steps.validate-secret.outputs.verification_result }} + steps: + - name: Checkout actions folder + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + with: + sparse-checkout: | + actions + persist-credentials: false + - name: Setup Scripts + uses: ./actions/setup + with: + destination: /opt/gh-aw/actions + - name: Checkout repository + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + with: + persist-credentials: false + - name: Create gh-aw temp directory + run: bash /opt/gh-aw/actions/create_gh_aw_tmp_dir.sh + - name: Configure Git credentials + env: + REPO_NAME: ${{ github.repository }} + SERVER_URL: ${{ github.server_url }} + run: | + git config --global user.email "github-actions[bot]@users.noreply.github.com" + git config --global user.name "github-actions[bot]" + # Re-authenticate git with GitHub token + SERVER_URL_STRIPPED="${SERVER_URL#https://}" + git remote set-url origin "https://x-access-token:${{ github.token }}@${SERVER_URL_STRIPPED}/${REPO_NAME}.git" + echo "Git configured with standard GitHub Actions identity" + - name: Checkout PR branch + id: checkout-pr + if: | + github.event.pull_request + uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8 + env: + GH_TOKEN: ${{ secrets.GH_AW_GITHUB_MCP_SERVER_TOKEN || secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} + with: + github-token: ${{ secrets.GH_AW_GITHUB_MCP_SERVER_TOKEN || secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} + script: | + const { setupGlobals } = require('/opt/gh-aw/actions/setup_globals.cjs'); + setupGlobals(core, github, context, exec, io); + const { main } = require('/opt/gh-aw/actions/checkout_pr_branch.cjs'); + await main(); + - name: Generate agentic run info + id: generate_aw_info + uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8 + with: + script: | + const fs = require('fs'); + + const awInfo = { + engine_id: "codex", + engine_name: "Codex", + model: process.env.GH_AW_MODEL_AGENT_CODEX || "", + version: "", + agent_version: "0.101.0", + workflow_name: "Test Versioned Prompt", + experimental: false, + supports_tools_allowlist: true, + prompt_version: "1.0.0", + run_id: context.runId, + run_number: context.runNumber, + run_attempt: process.env.GITHUB_RUN_ATTEMPT, + repository: context.repo.owner + '/' + context.repo.repo, + ref: context.ref, + sha: context.sha, + actor: context.actor, + event_name: context.eventName, + staged: false, + allowed_domains: ["defaults"], + firewall_enabled: true, + awf_version: "v0.19.1", + awmg_version: "v0.1.4", + steps: { + firewall: "squid" + }, + created_at: new Date().toISOString() + }; + + // Write to /tmp/gh-aw directory to avoid inclusion in PR + const tmpPath = '/tmp/gh-aw/aw_info.json'; + fs.writeFileSync(tmpPath, JSON.stringify(awInfo, null, 2)); + console.log('Generated aw_info.json at:', tmpPath); + console.log(JSON.stringify(awInfo, null, 2)); + + // Set model as output for reuse in other steps/jobs + core.setOutput('model', awInfo.model); + - name: Validate CODEX_API_KEY or OPENAI_API_KEY secret + id: validate-secret + run: /opt/gh-aw/actions/validate_multi_secret.sh CODEX_API_KEY OPENAI_API_KEY Codex https://github.github.com/gh-aw/reference/engines/#openai-codex + env: + CODEX_API_KEY: ${{ secrets.CODEX_API_KEY }} + OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }} + - name: Setup Node.js + uses: actions/setup-node@6044e13b5dc448c55e2357c09f80417699197238 # v6.2.0 + with: + node-version: '24' + package-manager-cache: false + - name: Install Codex + run: npm install -g --silent @openai/codex@0.101.0 + - name: Install awf binary + run: bash /opt/gh-aw/actions/install_awf_binary.sh v0.19.1 + - name: Determine automatic lockdown mode for GitHub MCP Server + id: determine-automatic-lockdown + uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8 + env: + GH_AW_GITHUB_TOKEN: ${{ secrets.GH_AW_GITHUB_TOKEN }} + GH_AW_GITHUB_MCP_SERVER_TOKEN: ${{ secrets.GH_AW_GITHUB_MCP_SERVER_TOKEN }} + with: + script: | + const determineAutomaticLockdown = require('/opt/gh-aw/actions/determine_automatic_lockdown.cjs'); + await determineAutomaticLockdown(github, context, core); + - name: Download container images + run: bash /opt/gh-aw/actions/download_docker_images.sh ghcr.io/github/gh-aw-firewall/agent:0.19.1 ghcr.io/github/gh-aw-firewall/api-proxy:0.19.1 ghcr.io/github/gh-aw-firewall/squid:0.19.1 ghcr.io/github/gh-aw-mcpg:v0.1.4 ghcr.io/github/github-mcp-server:v0.30.3 + - name: Start MCP Gateway + id: start-mcp-gateway + env: + GITHUB_MCP_LOCKDOWN: ${{ steps.determine-automatic-lockdown.outputs.lockdown == 'true' && '1' || '0' }} + GITHUB_MCP_SERVER_TOKEN: ${{ secrets.GH_AW_GITHUB_MCP_SERVER_TOKEN || secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} + run: | + set -eo pipefail + mkdir -p /tmp/gh-aw/mcp-config + + # Export gateway environment variables for MCP config and gateway script + export MCP_GATEWAY_PORT="80" + export MCP_GATEWAY_DOMAIN="host.docker.internal" + MCP_GATEWAY_API_KEY=$(openssl rand -base64 45 | tr -d '/+=') + echo "::add-mask::${MCP_GATEWAY_API_KEY}" + export MCP_GATEWAY_API_KEY + export MCP_GATEWAY_PAYLOAD_DIR="/tmp/gh-aw/mcp-payloads" + mkdir -p "${MCP_GATEWAY_PAYLOAD_DIR}" + export DEBUG="*" + + export GH_AW_ENGINE="codex" + export MCP_GATEWAY_DOCKER_COMMAND='docker run -i --rm --network host -v /var/run/docker.sock:/var/run/docker.sock -e MCP_GATEWAY_PORT -e MCP_GATEWAY_DOMAIN -e MCP_GATEWAY_API_KEY -e MCP_GATEWAY_PAYLOAD_DIR -e DEBUG -e MCP_GATEWAY_LOG_DIR -e GH_AW_MCP_LOG_DIR -e GH_AW_SAFE_OUTPUTS -e GH_AW_SAFE_OUTPUTS_CONFIG_PATH -e GH_AW_SAFE_OUTPUTS_TOOLS_PATH -e GH_AW_ASSETS_BRANCH -e GH_AW_ASSETS_MAX_SIZE_KB -e GH_AW_ASSETS_ALLOWED_EXTS -e DEFAULT_BRANCH -e GITHUB_MCP_SERVER_TOKEN -e GITHUB_MCP_LOCKDOWN -e GITHUB_REPOSITORY -e GITHUB_SERVER_URL -e GITHUB_SHA -e GITHUB_WORKSPACE -e GITHUB_TOKEN -e GITHUB_RUN_ID -e GITHUB_RUN_NUMBER -e GITHUB_RUN_ATTEMPT -e GITHUB_JOB -e GITHUB_ACTION -e GITHUB_EVENT_NAME -e GITHUB_EVENT_PATH -e GITHUB_ACTOR -e GITHUB_ACTOR_ID -e GITHUB_TRIGGERING_ACTOR -e GITHUB_WORKFLOW -e GITHUB_WORKFLOW_REF -e GITHUB_WORKFLOW_SHA -e GITHUB_REF -e GITHUB_REF_NAME -e GITHUB_REF_TYPE -e GITHUB_HEAD_REF -e GITHUB_BASE_REF -v /tmp/gh-aw/mcp-payloads:/tmp/gh-aw/mcp-payloads:rw -v /opt:/opt:ro -v /tmp:/tmp:rw -v '"${GITHUB_WORKSPACE}"':'"${GITHUB_WORKSPACE}"':rw ghcr.io/github/gh-aw-mcpg:v0.1.4' + + cat > /tmp/gh-aw/mcp-config/config.toml << GH_AW_MCP_CONFIG_EOF + [history] + persistence = "none" + + [shell_environment_policy] + inherit = "core" + include_only = ["CODEX_API_KEY", "GITHUB_PERSONAL_ACCESS_TOKEN", "HOME", "OPENAI_API_KEY", "PATH"] + + [mcp_servers.github] + user_agent = "test-versioned-prompt" + startup_timeout_sec = 120 + tool_timeout_sec = 60 + container = "ghcr.io/github/github-mcp-server:v0.30.3" + env = { "GITHUB_PERSONAL_ACCESS_TOKEN" = "$GH_AW_GITHUB_TOKEN", "GITHUB_READ_ONLY" = "1", "GITHUB_TOOLSETS" = "context,repos,issues,pull_requests" } + env_vars = ["GITHUB_PERSONAL_ACCESS_TOKEN", "GITHUB_READ_ONLY", "GITHUB_TOOLSETS"] + GH_AW_MCP_CONFIG_EOF + + # Generate JSON config for MCP gateway + cat << GH_AW_MCP_CONFIG_EOF | bash /opt/gh-aw/actions/start_mcp_gateway.sh + { + "mcpServers": { + "github": { + "container": "ghcr.io/github/github-mcp-server:v0.30.3", + "env": { + "GITHUB_LOCKDOWN_MODE": "$GITHUB_MCP_LOCKDOWN", + "GITHUB_PERSONAL_ACCESS_TOKEN": "$GITHUB_MCP_SERVER_TOKEN", + "GITHUB_READ_ONLY": "1", + "GITHUB_TOOLSETS": "context,repos,issues,pull_requests" + } + } + }, + "gateway": { + "port": $MCP_GATEWAY_PORT, + "domain": "${MCP_GATEWAY_DOMAIN}", + "apiKey": "${MCP_GATEWAY_API_KEY}", + "payloadDir": "${MCP_GATEWAY_PAYLOAD_DIR}" + } + } + GH_AW_MCP_CONFIG_EOF + - name: Generate workflow overview + uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8 + with: + script: | + const { generateWorkflowOverview } = require('/opt/gh-aw/actions/generate_workflow_overview.cjs'); + await generateWorkflowOverview(core); + - name: Download prompt artifact + uses: actions/download-artifact@018cc2cf5baa6db3ef3c5f8a56943fffe632ef53 # v6 + with: + name: prompt + path: /tmp/gh-aw/aw-prompts + - name: Clean git credentials + run: bash /opt/gh-aw/actions/clean_git_credentials.sh + - name: Run Codex + run: | + set -o pipefail + mkdir -p "$CODEX_HOME/logs" + sudo -E awf --env-all --container-workdir "${GITHUB_WORKSPACE}" --allow-domains 172.30.0.1,api.openai.com,api.snapcraft.io,archive.ubuntu.com,azure.archive.ubuntu.com,crl.geotrust.com,crl.globalsign.com,crl.identrust.com,crl.sectigo.com,crl.thawte.com,crl.usertrust.com,crl.verisign.com,crl3.digicert.com,crl4.digicert.com,crls.ssl.com,host.docker.internal,json-schema.org,json.schemastore.org,keyserver.ubuntu.com,ocsp.digicert.com,ocsp.geotrust.com,ocsp.globalsign.com,ocsp.identrust.com,ocsp.sectigo.com,ocsp.ssl.com,ocsp.thawte.com,ocsp.usertrust.com,ocsp.verisign.com,openai.com,packagecloud.io,packages.cloud.google.com,packages.microsoft.com,ppa.launchpad.net,s.symcb.com,s.symcd.com,security.ubuntu.com,ts-crl.ws.symantec.com,ts-ocsp.ws.symantec.com --log-level info --proxy-logs-dir /tmp/gh-aw/sandbox/firewall/logs --enable-host-access --image-tag 0.19.1 --skip-pull --enable-api-proxy \ + -- /bin/bash -c 'export PATH="$(find /opt/hostedtoolcache -maxdepth 4 -type d -name bin 2>/dev/null | tr '\''\n'\'' '\'':'\'')$PATH"; [ -n "$GOROOT" ] && export PATH="$GOROOT/bin:$PATH" || true && INSTRUCTION="$(cat /tmp/gh-aw/aw-prompts/prompt.txt)" && codex ${GH_AW_MODEL_DETECTION_CODEX:+-c model="$GH_AW_MODEL_DETECTION_CODEX" }exec --dangerously-bypass-approvals-and-sandbox --skip-git-repo-check "$INSTRUCTION"' 2>&1 | tee -a /tmp/gh-aw/agent-stdio.log + env: + CODEX_API_KEY: ${{ secrets.CODEX_API_KEY || secrets.OPENAI_API_KEY }} + CODEX_HOME: /tmp/gh-aw/mcp-config + GH_AW_MCP_CONFIG: /tmp/gh-aw/mcp-config/config.toml + GH_AW_MODEL_DETECTION_CODEX: ${{ vars.GH_AW_MODEL_DETECTION_CODEX || '' }} + GH_AW_PROMPT: /tmp/gh-aw/aw-prompts/prompt.txt + GITHUB_STEP_SUMMARY: ${{ env.GITHUB_STEP_SUMMARY }} + OPENAI_API_KEY: ${{ secrets.CODEX_API_KEY || secrets.OPENAI_API_KEY }} + RUST_LOG: trace,hyper_util=info,mio=info,reqwest=info,os_info=info,codex_otel=warn,codex_core=debug,ocodex_exec=debug + - name: Configure Git credentials + env: + REPO_NAME: ${{ github.repository }} + SERVER_URL: ${{ github.server_url }} + run: | + git config --global user.email "github-actions[bot]@users.noreply.github.com" + git config --global user.name "github-actions[bot]" + # Re-authenticate git with GitHub token + SERVER_URL_STRIPPED="${SERVER_URL#https://}" + git remote set-url origin "https://x-access-token:${{ github.token }}@${SERVER_URL_STRIPPED}/${REPO_NAME}.git" + echo "Git configured with standard GitHub Actions identity" + - name: Stop MCP Gateway + if: always() + continue-on-error: true + env: + MCP_GATEWAY_PORT: ${{ steps.start-mcp-gateway.outputs.gateway-port }} + MCP_GATEWAY_API_KEY: ${{ steps.start-mcp-gateway.outputs.gateway-api-key }} + GATEWAY_PID: ${{ steps.start-mcp-gateway.outputs.gateway-pid }} + run: | + bash /opt/gh-aw/actions/stop_mcp_gateway.sh "$GATEWAY_PID" + - name: Redact secrets in logs + if: always() + uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8 + with: + script: | + const { setupGlobals } = require('/opt/gh-aw/actions/setup_globals.cjs'); + setupGlobals(core, github, context, exec, io); + const { main } = require('/opt/gh-aw/actions/redact_secrets.cjs'); + await main(); + env: + GH_AW_SECRET_NAMES: 'CODEX_API_KEY,GH_AW_GITHUB_MCP_SERVER_TOKEN,GH_AW_GITHUB_TOKEN,GITHUB_TOKEN,OPENAI_API_KEY' + SECRET_CODEX_API_KEY: ${{ secrets.CODEX_API_KEY }} + SECRET_GH_AW_GITHUB_MCP_SERVER_TOKEN: ${{ secrets.GH_AW_GITHUB_MCP_SERVER_TOKEN }} + SECRET_GH_AW_GITHUB_TOKEN: ${{ secrets.GH_AW_GITHUB_TOKEN }} + SECRET_GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + SECRET_OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }} + - name: Upload engine output files + uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6 + with: + name: agent_outputs + path: | + /tmp/gh-aw/mcp-config/logs/ + /tmp/gh-aw/redacted-urls.log + if-no-files-found: ignore + - name: Parse agent logs for step summary + if: always() + uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8 + env: + GH_AW_AGENT_OUTPUT: /tmp/gh-aw/agent-stdio.log + with: + script: | + const { setupGlobals } = require('/opt/gh-aw/actions/setup_globals.cjs'); + setupGlobals(core, github, context, exec, io); + const { main } = require('/opt/gh-aw/actions/parse_codex_log.cjs'); + await main(); + - name: Parse MCP Gateway logs for step summary + if: always() + uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8 + with: + script: | + const { setupGlobals } = require('/opt/gh-aw/actions/setup_globals.cjs'); + setupGlobals(core, github, context, exec, io); + const { main } = require('/opt/gh-aw/actions/parse_mcp_gateway_log.cjs'); + await main(); + - name: Print firewall logs + if: always() + continue-on-error: true + env: + AWF_LOGS_DIR: /tmp/gh-aw/sandbox/firewall/logs + run: | + # Fix permissions on firewall logs so they can be uploaded as artifacts + # AWF runs with sudo, creating files owned by root + sudo chmod -R a+r /tmp/gh-aw/sandbox/firewall/logs 2>/dev/null || true + # Only run awf logs summary if awf command exists (it may not be installed if workflow failed before install step) + if command -v awf &> /dev/null; then + awf logs summary | tee -a "$GITHUB_STEP_SUMMARY" + else + echo 'AWF binary not installed, skipping firewall log summary' + fi + - name: Upload agent artifacts + if: always() + continue-on-error: true + uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6 + with: + name: agent-artifacts + path: | + /tmp/gh-aw/aw-prompts/prompt.txt + /tmp/gh-aw/aw_info.json + /tmp/gh-aw/mcp-logs/ + /tmp/gh-aw/sandbox/firewall/logs/ + /tmp/gh-aw/agent-stdio.log + /tmp/gh-aw/agent/ + if-no-files-found: ignore + diff --git a/.github/workflows/test-version-prompt.md b/.github/workflows/test-version-prompt.md new file mode 100644 index 00000000000..cc81c57d1bb --- /dev/null +++ b/.github/workflows/test-version-prompt.md @@ -0,0 +1,17 @@ +--- +name: Test Versioned Prompt +description: Example workflow demonstrating prompt versioning +version: 1.0.0 +engine: codex +on: + workflow_dispatch: +--- + +# Test Versioned Prompt + +This is a test workflow to demonstrate the new prompt versioning feature. + +The version (1.0.0) will be: +1. Displayed in the compiled workflow header as a comment +2. Included in the aw_info JSON for runtime tracking +3. Logged and available for comparison, rollback, and A/B testing diff --git a/docs/src/content/docs/reference/frontmatter-full.md b/docs/src/content/docs/reference/frontmatter-full.md index ea6cab61d6c..a4be5f6ba8a 100644 --- a/docs/src/content/docs/reference/frontmatter-full.md +++ b/docs/src/content/docs/reference/frontmatter-full.md @@ -42,6 +42,12 @@ source: "example-value" # (optional) tracker-id: "example-value" +# Prompt version using semantic versioning format (major.minor.patch with +# optional pre-release and build metadata). Used to track prompt evolution for +# testing, rollback, and A/B experiments. +# (optional) +version: "1.0.0" + # Optional array of labels to categorize and organize workflows. Labels can be # used to filter workflows in status/list commands. # (optional) diff --git a/docs/src/content/docs/reference/frontmatter.md b/docs/src/content/docs/reference/frontmatter.md index 30897156f94..8200e3fd6d9 100644 --- a/docs/src/content/docs/reference/frontmatter.md +++ b/docs/src/content/docs/reference/frontmatter.md @@ -56,6 +56,25 @@ Tracks workflow origin in format `owner/repo/path@ref`. Automatically populated source: "githubnext/agentics/workflows/ci-doctor.md@v1.0.0" ``` +### Prompt Versioning (`version:`) + +Semantic version string for tracking prompt evolution. Supports version comparison, rollback, and A/B testing. Follows [semantic versioning](https://semver.org/) format (major.minor.patch with optional pre-release and build metadata). + +```yaml wrap +version: "1.0.0" # Simple version +version: "2.1.3-beta.1" # With pre-release +version: "1.0.0+build.123" # With build metadata +``` + +The version appears in: +- Compiled workflow header as a comment (`# Prompt Version: 1.0.0`) +- Runtime `aw_info` JSON object (`prompt_version: "1.0.0"`) + +Use version increments to: +- Track major prompt changes (1.0.0 → 2.0.0) +- Document iterative improvements (1.0.0 → 1.1.0) +- Test experimental variants (1.0.0-beta.1) + ### Labels (`labels:`) Optional array of strings for categorizing and organizing workflows. Labels are displayed in `gh aw status` command output and can be filtered using the `--label` flag. diff --git a/pkg/parser/schemas/main_workflow_schema.json b/pkg/parser/schemas/main_workflow_schema.json index b5a7a65e026..275426a8eaa 100644 --- a/pkg/parser/schemas/main_workflow_schema.json +++ b/pkg/parser/schemas/main_workflow_schema.json @@ -33,6 +33,12 @@ "description": "Optional tracker identifier to tag all created assets (issues, discussions, comments, pull requests). Must be at least 8 characters and contain only alphanumeric characters, hyphens, and underscores. This identifier will be inserted in the body/description of all created assets to enable searching and retrieving assets associated with this workflow.", "examples": ["workflow-2024-q1", "team-alpha-bot", "security_audit_v2"] }, + "version": { + "type": "string", + "pattern": "^\\d+\\.\\d+\\.\\d+(-[a-zA-Z0-9.-]+)?(\\+[a-zA-Z0-9.-]+)?$", + "description": "Prompt version using semantic versioning format (major.minor.patch with optional pre-release and build metadata). Used to track prompt evolution for testing, rollback, and A/B experiments.", + "examples": ["1.0.0", "2.1.3", "1.0.0-beta.1", "1.2.3-alpha.1+build.123"] + }, "labels": { "type": "array", "description": "Optional array of labels to categorize and organize workflows. Labels can be used to filter workflows in status/list commands.", diff --git a/pkg/workflow/compiler_orchestrator_workflow.go b/pkg/workflow/compiler_orchestrator_workflow.go index 8ccab3c5502..4c30f9cd5e1 100644 --- a/pkg/workflow/compiler_orchestrator_workflow.go +++ b/pkg/workflow/compiler_orchestrator_workflow.go @@ -128,6 +128,7 @@ func (c *Compiler) buildInitialWorkflowData( FrontmatterYAML: strings.Join(result.FrontmatterLines, "\n"), Description: c.extractDescription(result.Frontmatter), Source: c.extractSource(result.Frontmatter), + Version: c.extractVersion(result.Frontmatter), TrackerID: toolsResult.trackerID, ImportedFiles: importsResult.ImportedFiles, ImportedMarkdown: toolsResult.importedMarkdown, // Only imports WITH inputs diff --git a/pkg/workflow/compiler_types.go b/pkg/workflow/compiler_types.go index c7fd19ee9fb..67bf4c41f80 100644 --- a/pkg/workflow/compiler_types.go +++ b/pkg/workflow/compiler_types.go @@ -385,6 +385,7 @@ type WorkflowData struct { FrontmatterYAML string // raw frontmatter YAML content (rendered as comment in lock file for reference) Description string // optional description rendered as comment in lock file Source string // optional source field (owner/repo@ref/path) rendered as comment in lock file + Version string // optional semantic version for the prompt (e.g., "1.0.0", "2.1.3-beta.1") TrackerID string // optional tracker identifier for created assets (min 8 chars, alphanumeric + hyphens/underscores) ImportedFiles []string // list of files imported via imports field (rendered as comment in lock file) ImportedMarkdown string // Only imports WITH inputs (for compile-time substitution) diff --git a/pkg/workflow/compiler_yaml.go b/pkg/workflow/compiler_yaml.go index b51db73eb19..3feb48d1a3b 100644 --- a/pkg/workflow/compiler_yaml.go +++ b/pkg/workflow/compiler_yaml.go @@ -76,6 +76,12 @@ func (c *Compiler) generateWorkflowHeader(yaml *strings.Builder, data *WorkflowD fmt.Fprintf(yaml, "# Source: %s\n", cleanSource) } + // Add version comment if provided + if data.Version != "" { + yaml.WriteString("#\n") + fmt.Fprintf(yaml, "# Prompt Version: %s\n", data.Version) + } + // Add manifest of imported/included files if any exist if len(data.ImportedFiles) > 0 || len(data.IncludedFiles) > 0 { yaml.WriteString("#\n") @@ -511,6 +517,11 @@ func (c *Compiler) generateCreateAwInfo(yaml *strings.Builder, data *WorkflowDat fmt.Fprintf(yaml, " experimental: %t,\n", engine.IsExperimental()) fmt.Fprintf(yaml, " supports_tools_allowlist: %t,\n", engine.SupportsToolsAllowlist()) + // Prompt version (if specified) + if data.Version != "" { + fmt.Fprintf(yaml, " prompt_version: \"%s\",\n", data.Version) + } + // Run metadata yaml.WriteString(" run_id: context.runId,\n") yaml.WriteString(" run_number: context.runNumber,\n") diff --git a/pkg/workflow/frontmatter_extraction_metadata.go b/pkg/workflow/frontmatter_extraction_metadata.go index b621c8c9d6e..d4b10121bc9 100644 --- a/pkg/workflow/frontmatter_extraction_metadata.go +++ b/pkg/workflow/frontmatter_extraction_metadata.go @@ -68,6 +68,24 @@ func (c *Compiler) extractSource(frontmatter map[string]any) string { return "" } +// extractVersion extracts the version field from frontmatter +func (c *Compiler) extractVersion(frontmatter map[string]any) string { + value, exists := frontmatter["version"] + if !exists { + return "" + } + + // Convert the value to string + if strValue, ok := value.(string); ok { + version := strings.TrimSpace(strValue) + frontmatterMetadataLog.Printf("Extracted version: %s", version) + return version + } + + frontmatterMetadataLog.Printf("Version field is not a string: type=%T", value) + return "" +} + // extractTrackerID extracts and validates the tracker-id field from frontmatter func (c *Compiler) extractTrackerID(frontmatter map[string]any) (string, error) { value, exists := frontmatter["tracker-id"] diff --git a/pkg/workflow/version_prompt_test.go b/pkg/workflow/version_prompt_test.go new file mode 100644 index 00000000000..49e3b593e84 --- /dev/null +++ b/pkg/workflow/version_prompt_test.go @@ -0,0 +1,212 @@ +//go:build !integration + +package workflow + +import ( + "strings" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +// TestPromptVersionExtraction tests extraction of version field from frontmatter +func TestPromptVersionExtraction(t *testing.T) { + t.Run("valid semantic version", func(t *testing.T) { + compiler := NewCompiler() + frontmatter := map[string]any{ + "version": "1.0.0", + } + + version := compiler.extractVersion(frontmatter) + assert.Equal(t, "1.0.0", version, "Should extract version field") + }) + + t.Run("version with pre-release", func(t *testing.T) { + compiler := NewCompiler() + frontmatter := map[string]any{ + "version": "2.1.3-beta.1", + } + + version := compiler.extractVersion(frontmatter) + assert.Equal(t, "2.1.3-beta.1", version, "Should extract version with pre-release") + }) + + t.Run("version with build metadata", func(t *testing.T) { + compiler := NewCompiler() + frontmatter := map[string]any{ + "version": "1.0.0+build.123", + } + + version := compiler.extractVersion(frontmatter) + assert.Equal(t, "1.0.0+build.123", version, "Should extract version with build metadata") + }) + + t.Run("version with pre-release and build", func(t *testing.T) { + compiler := NewCompiler() + frontmatter := map[string]any{ + "version": "1.2.3-alpha.1+build.123", + } + + version := compiler.extractVersion(frontmatter) + assert.Equal(t, "1.2.3-alpha.1+build.123", version, "Should extract complete version string") + }) + + t.Run("missing version field", func(t *testing.T) { + compiler := NewCompiler() + frontmatter := map[string]any{ + "name": "Test Workflow", + } + + version := compiler.extractVersion(frontmatter) + assert.Equal(t, "", version, "Should return empty string when version not present") + }) + + t.Run("non-string version field", func(t *testing.T) { + compiler := NewCompiler() + frontmatter := map[string]any{ + "version": 123, + } + + version := compiler.extractVersion(frontmatter) + assert.Equal(t, "", version, "Should return empty string for non-string version") + }) + + t.Run("version with whitespace", func(t *testing.T) { + compiler := NewCompiler() + frontmatter := map[string]any{ + "version": " 1.0.0 ", + } + + version := compiler.extractVersion(frontmatter) + assert.Equal(t, "1.0.0", version, "Should trim whitespace from version") + }) +} + +// TestPromptVersionInHeader tests that version appears in compiled workflow header +func TestPromptVersionInHeader(t *testing.T) { + t.Run("version included in workflow header", func(t *testing.T) { + workflowData := &WorkflowData{ + Name: "Test Workflow", + Version: "1.2.3", + AI: "codex", + On: "workflow_dispatch:", + } + + compiler := NewCompiler() + yaml, err := compiler.generateYAML(workflowData, "test.md") + require.NoError(t, err, "Should generate YAML successfully") + + assert.Contains(t, yaml, "# Prompt Version: 1.2.3", "Version should appear in header comment") + }) + + t.Run("no version comment when version not set", func(t *testing.T) { + workflowData := &WorkflowData{ + Name: "Test Workflow", + AI: "codex", + On: "workflow_dispatch:", + } + + compiler := NewCompiler() + yaml, err := compiler.generateYAML(workflowData, "test.md") + require.NoError(t, err, "Should generate YAML successfully") + + assert.NotContains(t, yaml, "# Prompt Version:", "Version comment should not appear when version not set") + }) +} + +// TestPromptVersionInAwInfo tests that version appears in aw_info JSON +func TestPromptVersionInAwInfo(t *testing.T) { + t.Run("prompt_version in aw_info", func(t *testing.T) { + workflowData := &WorkflowData{ + Name: "Test Workflow", + Version: "2.0.0-beta.1", + AI: "codex", + On: "workflow_dispatch:", + } + + compiler := NewCompiler() + yaml, err := compiler.generateYAML(workflowData, "test.md") + require.NoError(t, err, "Should generate YAML successfully") + + assert.Contains(t, yaml, `prompt_version: "2.0.0-beta.1"`, "prompt_version should appear in aw_info JSON") + }) + + t.Run("no prompt_version when version not set", func(t *testing.T) { + workflowData := &WorkflowData{ + Name: "Test Workflow", + AI: "codex", + On: "workflow_dispatch:", + } + + compiler := NewCompiler() + yaml, err := compiler.generateYAML(workflowData, "test.md") + require.NoError(t, err, "Should generate YAML successfully") + + assert.NotContains(t, yaml, "prompt_version:", "prompt_version should not appear when version not set") + }) +} + +// TestPromptVersionWithOtherMetadata tests version works with other metadata fields +func TestPromptVersionWithOtherMetadata(t *testing.T) { + workflowData := &WorkflowData{ + Name: "Test Workflow", + Description: "A test workflow for versioning", + Source: "github/gh-aw/workflows/test.md@main", + Version: "3.1.4", + TrackerID: "test-tracker-123", + AI: "codex", + On: "workflow_dispatch:", + } + + compiler := NewCompiler() + yaml, err := compiler.generateYAML(workflowData, "test.md") + require.NoError(t, err, "Should generate YAML successfully") + + assert.Contains(t, yaml, "# A test workflow for versioning", "Description should appear in header") + assert.Contains(t, yaml, "# Source: github/gh-aw/workflows/test.md@main", "Source should appear in header") + assert.Contains(t, yaml, "# Prompt Version: 3.1.4", "Version should appear in header") +} + +// TestPromptVersionSchemaValidation tests schema validation of version field +func TestPromptVersionSchemaValidation(t *testing.T) { + tests := []struct { + name string + version string + shouldMatch bool + }{ + {"simple semver", "1.0.0", true}, + {"double digit major", "10.5.3", true}, + {"triple digit minor", "1.100.3", true}, + {"large patch", "1.0.999", true}, + {"with pre-release", "1.0.0-alpha", true}, + {"with numeric pre-release", "1.0.0-beta.1", true}, + {"with build metadata", "1.0.0+20240101", true}, + {"with both pre-release and build", "1.0.0-rc.1+build.123", true}, + {"complex pre-release", "1.0.0-alpha.beta.1", true}, + {"complex build metadata", "1.0.0+build.123.456", true}, + {"missing patch", "1.0", false}, + {"missing minor and patch", "1", false}, + {"leading v", "v1.0.0", false}, + {"non-numeric major", "x.0.0", false}, + {"non-numeric minor", "1.x.0", false}, + {"non-numeric patch", "1.0.x", false}, + {"leading zero in major", "01.0.0", false}, + {"leading zero in minor", "1.01.0", false}, + {"leading zero in patch", "1.0.01", false}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if tt.shouldMatch { + // For valid versions, just ensure they have 3 numeric parts + basePart := strings.Split(strings.Split(tt.version, "-")[0], "+")[0] + parts := strings.Split(basePart, ".") + assert.Equal(t, 3, len(parts), "Valid version should have 3 parts") + } else { + // For invalid versions, document the pattern they violate + t.Logf("Invalid version '%s' correctly identified as not matching semver pattern", tt.version) + } + }) + } +}