Skip to content

maintenance-prs

maintenance-prs #5

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('<', '&lt;')
.replaceAll('>', '&gt;');
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 }}