Repository navigation
maintenance-prs #5
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| name: maintenance-prs | |
| on: | |
| workflow_dispatch: | |
| inputs: | |
| diagnose: | |
| description: Check proxy and model access without running maintenance | |
| type: boolean | |
| default: false | |
| schedule: | |
| # Everyday at 00:00 UTC | |
| - cron: '0 0 * * *' | |
| permissions: | |
| contents: write | |
| issues: read | |
| pull-requests: write | |
| env: | |
| PNPM_VERSION: '10.11.1' | |
| SHOPIFY_CLI_ENV: development | |
| SHOPIFY_CONFIG: debug | |
| jobs: | |
| diagnose: | |
| name: Diagnose Claude model access | |
| if: github.repository == 'Shopify/cli' && inputs.diagnose | |
| runs-on: ubuntu-latest | |
| timeout-minutes: 5 | |
| permissions: {} | |
| steps: | |
| - name: Check proxy responses | |
| uses: actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea # v7.0.1 | |
| env: | |
| ANTHROPIC_BASE_URL: ${{ secrets.ANTHROPIC_BASE_URL }} | |
| ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }} | |
| with: | |
| script: | | |
| const baseUrl = (process.env.ANTHROPIC_BASE_URL || '').trim().replace(/\/$/, ''); | |
| const apiKey = (process.env.ANTHROPIC_API_KEY || '').trim(); | |
| if (!baseUrl || !apiKey) { | |
| core.setFailed('ANTHROPIC_BASE_URL and ANTHROPIC_API_KEY must both be set.'); | |
| return; | |
| } | |
| core.setSecret(baseUrl); | |
| core.setSecret(apiKey); | |
| if (new URL(baseUrl).origin !== 'https://proxy.shopify.ai') { | |
| core.setFailed('ANTHROPIC_BASE_URL must point to the Shopify AI proxy.'); | |
| return; | |
| } | |
| core.info(`Using managed Anthropic route: ${baseUrl === 'https://proxy.shopify.ai/apis/anthropic'}`); | |
| const configuredUrl = new URL(baseUrl); | |
| const knownRoutes = new Map([ | |
| ['/apis/anthropic', 'managed Anthropic base'], | |
| ['/apis/anthropic/v1', 'managed Anthropic base with v1 already appended'], | |
| ['/vendors/anthropic-claude-code', 'Claude Code vendor base'], | |
| ['/vendors/anthropic', 'Anthropic vendor base'], | |
| ['/', 'proxy root'], | |
| ]); | |
| core.info(`Configured route category: ${knownRoutes.get(configuredUrl.pathname.replace(/\/$/, '') || '/') ?? 'other route'}`); | |
| core.info(`URL has query or fragment: ${Boolean(configuredUrl.search || configuredUrl.hash)}`); | |
| const headers = { | |
| 'x-api-key': apiKey, | |
| 'anthropic-version': '2023-06-01', | |
| 'content-type': 'application/json', | |
| }; | |
| async function probe(label, url, options = {}) { | |
| try { | |
| const response = await fetch(url, { | |
| ...options, redirect: 'error', signal: AbortSignal.timeout(60000), | |
| }); | |
| const data = await response.json().catch(() => ({})); | |
| core.info(`${label}: HTTP ${response.status}`); | |
| if (!response.ok) { | |
| const error = data.error; | |
| // The request contains only a fixed test prompt. Never print headers or the full response. | |
| core.info(`Error type: ${JSON.stringify(error?.type ?? data.type ?? 'unknown')}`); | |
| core.info(`Error message: ${JSON.stringify(String(error?.message ?? data.message ?? 'No JSON error message').slice(0, 1000))}`); | |
| } | |
| return {ok: response.ok, data}; | |
| } catch { | |
| core.warning(`${label}: network failure, redirect, or timeout.`); | |
| return {ok: false, data: {}}; | |
| } | |
| } | |
| const catalogue = await probe('Model catalogue', 'https://proxy.shopify.ai/v1/models', { | |
| headers: {authorization: `Bearer ${apiKey}`}, | |
| }); | |
| if (Array.isArray(catalogue.data.data)) { | |
| const ids = catalogue.data.data.map((model) => model.id).filter((id) => typeof id === 'string'); | |
| core.info(`Opus latest in catalogue: ${ids.includes('claude-opus-latest')}`); | |
| core.info(`Opus 5 in catalogue: ${ids.includes('claude-opus-5')}`); | |
| } | |
| await probe('Model lookup', `${baseUrl}/v1/models/claude-opus-latest`, {headers}); | |
| const testRequest = { | |
| method: 'POST', headers, | |
| body: JSON.stringify({ | |
| model: 'claude-opus-latest', max_tokens: 32, | |
| messages: [{role: 'user', content: 'Reply with OK.'}], | |
| }), | |
| }; | |
| const message = await probe('Minimal model request', `${baseUrl}/v1/messages`, testRequest); | |
| if (baseUrl !== 'https://proxy.shopify.ai/apis/anthropic') { | |
| const managed = await probe('Managed Anthropic model request', 'https://proxy.shopify.ai/apis/anthropic/v1/messages', testRequest); | |
| if (managed.ok && managed.data.type === 'message') { | |
| core.warning('The managed route works with this token and model. Update ANTHROPIC_BASE_URL to the managed Anthropic base URL.'); | |
| return; | |
| } | |
| } | |
| if (!message.ok || message.data.type !== 'message') { | |
| core.setFailed('The direct model request failed. Inspect the HTTP status and error above.'); | |
| } | |
| maintenance: | |
| name: Maintenance - ${{ matrix.task }} | |
| if: github.repository == 'Shopify/cli' && !inputs.diagnose | |
| runs-on: ubuntu-latest | |
| timeout-minutes: 120 | |
| strategy: | |
| fail-fast: false | |
| matrix: | |
| task: [refactor, tests, performance, security] | |
| concurrency: | |
| group: maintenance-prs-${{ matrix.task }} | |
| cancel-in-progress: false | |
| env: | |
| MAINTENANCE_TASK: ${{ matrix.task }} | |
| MAINTENANCE_BRANCH: ${{ matrix.task }}-maintenance-${{ github.run_id }} | |
| steps: | |
| - name: Check task schedule | |
| id: schedule | |
| run: | | |
| if [[ "$GITHUB_EVENT_NAME" == 'workflow_dispatch' || | |
| "$MAINTENANCE_TASK" == 'refactor' || | |
| "$MAINTENANCE_TASK" == 'tests' || | |
| "$(date -u +%u)" == '1' ]]; then | |
| echo 'run=true' >> "$GITHUB_OUTPUT" | |
| else | |
| echo 'run=false' >> "$GITHUB_OUTPUT" | |
| echo "Skipping $MAINTENANCE_TASK until Monday." | |
| fi | |
| - uses: actions/checkout@v6 | |
| if: steps.schedule.outputs.run == 'true' | |
| with: | |
| # Include remote branches and history for the prompts' duplicate checks. | |
| fetch-depth: 0 | |
| - name: Setup deps | |
| if: steps.schedule.outputs.run == 'true' | |
| uses: ./.github/actions/setup-cli-deps | |
| with: | |
| node-version: '26.1.0' | |
| - name: Create maintenance branch | |
| if: steps.schedule.outputs.run == 'true' | |
| run: | | |
| git config user.name 'github-actions[bot]' | |
| git config user.email '41898282+github-actions[bot]@users.noreply.github.com' | |
| git checkout -b "$MAINTENANCE_BRANCH" | |
| - name: Run maintenance task | |
| id: maintenance | |
| if: steps.schedule.outputs.run == 'true' | |
| uses: anthropics/claude-code-action@36a69b6a90b850823f86de06fdfd56264772ad98 # v1 | |
| env: | |
| ANTHROPIC_BASE_URL: ${{ secrets.ANTHROPIC_BASE_URL }} | |
| GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} | |
| CLAUDE_BRANCH: ${{ env.MAINTENANCE_BRANCH }} | |
| with: | |
| anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY }} | |
| github_token: ${{ secrets.GITHUB_TOKEN }} | |
| bot_name: 'github-actions[bot]' | |
| bot_id: '41898282' | |
| prompt: | | |
| Perform the maintenance task in `.agents/automated-tasks/${{ matrix.task }}.md`. | |
| Read the entire file, `AGENTS.md`, and `.github/PULL_REQUEST_TEMPLATE.md` | |
| before choosing a change. Carefully follow every phase and boundary in the task file. | |
| Work on the existing branch `${{ env.MAINTENANCE_BRANCH }}`. It already has | |
| the required task prefix. Create at most ONE draft PR against the repository's | |
| default branch. Use `gh pr create --draft` and the PR template exactly as instructed. | |
| Never merge, approve, or mark a PR ready for review. | |
| For duplicate checks, inspect remote branches and search open, merged, and closed | |
| PRs with `gh pr list --state all`, then read the bodies and diffs of related PRs. | |
| This includes previous maintenance PRs whose branches have been deleted. | |
| If no worthwhile, non-duplicate change exists after the required candidate checks, | |
| stop successfully without opening a PR. | |
| Dependencies are installed. Run all verification required by the task before | |
| opening the PR. Do not open a PR if the required checks fail. | |
| Keep duplicate-check notes out of the PR body and all template checkboxes unchecked. | |
| Do not post to Slack; the workflow will announce the PR after it exists. | |
| claude_args: | | |
| --model claude-opus-latest | |
| --allowedTools Read,Glob,Grep,Edit,Write,Bash | |
| - name: Find the created PR | |
| id: pull-request | |
| # Claude may create a PR before a later step in its session fails. | |
| if: ${{ !cancelled() && steps.maintenance.outcome != 'skipped' }} | |
| uses: actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea # v7.0.1 | |
| with: | |
| script: | | |
| const {data: pullRequests} = await github.rest.pulls.list({ | |
| ...context.repo, | |
| state: 'open', | |
| head: `${context.repo.owner}:${process.env.MAINTENANCE_BRANCH}`, | |
| base: context.payload.repository.default_branch, | |
| }); | |
| const pullRequest = pullRequests[0]; | |
| if (!pullRequest) { | |
| core.info('No maintenance PR was created.'); | |
| return; | |
| } | |
| const pullRequestTitle = pullRequest.title | |
| .replaceAll('&', '&') | |
| .replaceAll('<', '<') | |
| .replaceAll('>', '>'); | |
| core.setOutput('payload', JSON.stringify({ | |
| channel: 'C0ARV62K59C', // #devtools-gardener-backlog | |
| text: `Maintenance PR (${process.env.MAINTENANCE_TASK}): <${pullRequest.html_url}|${pullRequestTitle}>`, | |
| unfurl_links: false, | |
| unfurl_media: false, | |
| })); | |
| - name: Post PR to gardener backlog | |
| if: ${{ !cancelled() && steps.pull-request.outputs.payload != '' }} | |
| uses: slackapi/slack-github-action@b0fa283ad8fea605de13dc3f449259339835fc52 # v2.1.0 | |
| with: | |
| method: chat.postMessage | |
| token: ${{ secrets.SLACK_GARDENER_BOT_TOKEN }} | |
| payload: ${{ steps.pull-request.outputs.payload }} |