mirror of
https://github.com/zhom/donutbrowser.git
synced 2026-10-22 07:08:53 +02:00
ci: free model for AI jobs
This commit is contained in:
1 parent
e14e97963d
commit
4388f6ae74
12 files changed
+195
-301
No files matched your search
@@ -13,14 +13,6 @@ on:
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
env:
|
||||
# GitHub Models (models.github.ai) was retired on 2026-07-30, and every call
|
||||
# to it here had degraded since. Both AI jobs now ask GitHub Copilot through
|
||||
# the workflow token (`copilot-requests: write`). `auto` lets Copilot pick:
|
||||
# it is the only value every Copilot plan accepts and it cannot go stale the
|
||||
# way the pinned `openai/gpt-4.1` did.
|
||||
COPILOT_MODEL: auto
|
||||
|
||||
jobs:
|
||||
# Decides what happens to a new issue before triage answers it. Minor
|
||||
# problems never close an issue: a feature filed as a bug (or the reverse)
|
||||
@@ -39,7 +31,6 @@ jobs:
|
||||
permissions:
|
||||
contents: read
|
||||
issues: write
|
||||
copilot-requests: write
|
||||
outputs:
|
||||
action: ${{ steps.decide.outputs.action }}
|
||||
steps:
|
||||
@@ -97,27 +88,21 @@ jobs:
|
||||
cat /tmp/issue-body.txt
|
||||
} > /tmp/user.txt
|
||||
|
||||
# continue-on-error: an install failure is an outage like any other.
|
||||
# The Copilot step records it and the run turns red at the end.
|
||||
- name: Set up Copilot CLI
|
||||
continue-on-error: true
|
||||
uses: ./.github/actions/setup-copilot-cli
|
||||
|
||||
- name: Ask Copilot
|
||||
- name: Ask the model
|
||||
env:
|
||||
COPILOT_GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
AI_API_KEY: ${{ secrets.OPENROUTER_API_KEY }}
|
||||
run: |
|
||||
# Fail-open: an outage or an answer that is not JSON must never close
|
||||
# or delete a contributor's issue. It is recorded and turns the run red below.
|
||||
if ! bash .github/scripts/copilot-infer.sh /tmp/system.txt /tmp/user.txt /tmp/raw.txt; then
|
||||
printf '%s\n' "the Copilot call failed" >> /tmp/ai-degraded
|
||||
if ! bash .github/scripts/ai-infer.sh /tmp/system.txt /tmp/user.txt /tmp/raw.txt; then
|
||||
printf '%s\n' "the model call failed" >> /tmp/ai-degraded
|
||||
echo '{"action": "keep", "kind": "unclear"}' > /tmp/result.json
|
||||
exit 0
|
||||
fi
|
||||
|
||||
if ! python3 .github/scripts/extract-json.py /tmp/raw.txt /tmp/result.json; then
|
||||
echo "::error::Copilot returned no JSON object; keeping the issue"
|
||||
printf '%s\n' "Copilot returned output that was not JSON" >> /tmp/ai-degraded
|
||||
echo "::error::The model returned no JSON object; keeping the issue"
|
||||
printf '%s\n' "the model returned output that was not JSON" >> /tmp/ai-degraded
|
||||
echo '{"action": "keep", "kind": "unclear"}' > /tmp/result.json
|
||||
fi
|
||||
|
||||
@@ -136,7 +121,7 @@ jobs:
|
||||
action = r.get('action')
|
||||
if action not in ('keep', 'close', 'delete'):
|
||||
with open('/tmp/ai-degraded', 'a') as fh:
|
||||
fh.write('Copilot returned no known action; the issue was kept open\n')
|
||||
fh.write('The model returned no known action; the issue was kept open\n')
|
||||
action = 'keep'
|
||||
kind = r.get('kind')
|
||||
reasons = [' '.join(str(x).split())[:300] for x in (r.get('reasons') or []) if str(x).strip()][:10]
|
||||
@@ -147,7 +132,7 @@ jobs:
|
||||
issue_text = normalize(open('/tmp/issue-title.txt').read() + '\n' + open('/tmp/issue-body.txt').read())
|
||||
if len(quote) < 2 or quote not in issue_text:
|
||||
with open('/tmp/ai-degraded', 'a') as fh:
|
||||
fh.write('Copilot asked to delete the issue without quoting it; it was kept open, check it by hand\n')
|
||||
fh.write('The model asked to delete the issue without quoting it; it was kept open, check it by hand\n')
|
||||
action = 'keep'
|
||||
else:
|
||||
open('/tmp/insult-quote.txt', 'w').write(quote)
|
||||
@@ -256,19 +241,12 @@ jobs:
|
||||
permissions:
|
||||
contents: read
|
||||
issues: write
|
||||
copilot-requests: write
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 #v7.0.1
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
# continue-on-error: an install failure is an outage like any other.
|
||||
# The Copilot steps record it and the run turns red at the end.
|
||||
- name: Set up Copilot CLI
|
||||
continue-on-error: true
|
||||
uses: ./.github/actions/setup-copilot-cli
|
||||
|
||||
- name: Check if first-time contributor
|
||||
id: check-first-time
|
||||
env:
|
||||
@@ -460,7 +438,7 @@ jobs:
|
||||
|
||||
- name: Stage 1 (triage and file selection)
|
||||
env:
|
||||
COPILOT_GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
AI_API_KEY: ${{ secrets.OPENROUTER_API_KEY }}
|
||||
run: |
|
||||
# The triage call returns ONLY JSON. It classifies the issue and picks a
|
||||
# short list of source files for the composer to read.
|
||||
@@ -477,14 +455,14 @@ jobs:
|
||||
|
||||
# An outage must not fail the run here: fall through to the safe
|
||||
# classification below and let the last step report it.
|
||||
if bash .github/scripts/copilot-infer.sh /tmp/triage-system.txt /tmp/triage-user.txt /tmp/triage-raw.txt; then
|
||||
if bash .github/scripts/ai-infer.sh /tmp/triage-system.txt /tmp/triage-user.txt /tmp/triage-raw.txt; then
|
||||
if ! python3 .github/scripts/extract-json.py /tmp/triage-raw.txt /tmp/triage.json; then
|
||||
echo "::error::Triage returned no JSON object; using fallback classification"
|
||||
printf '%s\n' "triage returned output that was not JSON" >> /tmp/ai-degraded
|
||||
rm -f /tmp/triage.json
|
||||
fi
|
||||
else
|
||||
printf '%s\n' "the triage Copilot call failed" >> /tmp/ai-degraded
|
||||
printf '%s\n' "the triage model call failed" >> /tmp/ai-degraded
|
||||
fi
|
||||
|
||||
if [ ! -s /tmp/triage.json ]; then
|
||||
@@ -608,7 +586,7 @@ jobs:
|
||||
- name: Stage 2 (compose response)
|
||||
id: compose
|
||||
env:
|
||||
COPILOT_GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
AI_API_KEY: ${{ secrets.OPENROUTER_API_KEY }}
|
||||
ISSUE_AUTHOR: ${{ github.event.issue.user.login }}
|
||||
IS_FIRST_TIME: ${{ steps.check-first-time.outputs.is_first_time }}
|
||||
run: |
|
||||
@@ -631,9 +609,9 @@ jobs:
|
||||
|
||||
# Same as triage: an outage must not fail the run. When no comment can
|
||||
# be composed the remaining steps are skipped instead.
|
||||
if ! bash .github/scripts/copilot-infer.sh /tmp/composer-system.txt /tmp/composer-user.txt /tmp/ai-comment.txt; then
|
||||
if ! bash .github/scripts/ai-infer.sh /tmp/composer-system.txt /tmp/composer-user.txt /tmp/ai-comment.txt; then
|
||||
echo "::error::Skipping the triage comment"
|
||||
printf '%s\n' "the composer Copilot call failed" >> /tmp/ai-degraded
|
||||
printf '%s\n' "the composer model call failed" >> /tmp/ai-degraded
|
||||
echo "has_comment=false" >> "$GITHUB_OUTPUT"
|
||||
exit 0
|
||||
fi
|
||||
@@ -691,7 +669,6 @@ jobs:
|
||||
permissions:
|
||||
contents: read
|
||||
pull-requests: write
|
||||
copilot-requests: write
|
||||
steps:
|
||||
# pull_request_target runs in the base repository's context: this is the
|
||||
# trusted base branch. The pull request is only ever read as text.
|
||||
@@ -700,12 +677,6 @@ jobs:
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
# continue-on-error: an install failure is an outage like any other.
|
||||
# The Copilot step records it and the run turns red at the end.
|
||||
- name: Set up Copilot CLI
|
||||
continue-on-error: true
|
||||
uses: ./.github/actions/setup-copilot-cli
|
||||
|
||||
- name: Check if first-time contributor
|
||||
id: check-first-time
|
||||
env:
|
||||
@@ -771,7 +742,7 @@ jobs:
|
||||
- name: Analyze PR with AI
|
||||
id: analyze
|
||||
env:
|
||||
COPILOT_GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
AI_API_KEY: ${{ secrets.OPENROUTER_API_KEY }}
|
||||
PR_TITLE: ${{ github.event.pull_request.title }}
|
||||
PR_BODY: ${{ github.event.pull_request.body }}
|
||||
PR_AUTHOR: ${{ github.event.pull_request.user.login }}
|
||||
@@ -835,9 +806,9 @@ jobs:
|
||||
|
||||
# An outage must not red-cross an otherwise healthy pull request here:
|
||||
# skip the review comment; the last step reports the degradation.
|
||||
if ! bash .github/scripts/copilot-infer.sh /tmp/pr-system.txt /tmp/pr-user.txt /tmp/ai-comment.txt; then
|
||||
if ! bash .github/scripts/ai-infer.sh /tmp/pr-system.txt /tmp/pr-user.txt /tmp/ai-comment.txt; then
|
||||
echo "::error::Skipping the review comment"
|
||||
printf '%s\n' "the PR review Copilot call failed" >> /tmp/ai-degraded
|
||||
printf '%s\n' "the PR review model call failed" >> /tmp/ai-degraded
|
||||
echo "has_comment=false" >> "$GITHUB_OUTPUT"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
@@ -22,7 +22,6 @@ on:
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
copilot-requests: write
|
||||
|
||||
jobs:
|
||||
notify:
|
||||
@@ -126,31 +125,15 @@ jobs:
|
||||
echo "previous-tag=${PREV_TAG}" >> "$GITHUB_OUTPUT"
|
||||
echo "Collected $(wc -l < commits.txt) commits between ${PREV_TAG} and ${TAG}."
|
||||
|
||||
# The Copilot CLI is not preinstalled on GitHub-hosted runners, and
|
||||
# ai-inference v3 shells out to it.
|
||||
- name: Set up Copilot CLI
|
||||
id: copilot
|
||||
if: steps.gate.outputs.skip != 'true'
|
||||
uses: ./.github/actions/setup-copilot-cli
|
||||
|
||||
- name: Generate summary with AI
|
||||
id: ai
|
||||
if: steps.gate.outputs.skip != 'true'
|
||||
uses: actions/ai-inference@2c43c91ae16266ca159d311430343c67a5ffa222 # v3
|
||||
with:
|
||||
prompt-file: .github/prompts/telegram-release-summary.prompt.yml
|
||||
input: |
|
||||
version: ${{ steps.tag.outputs.tag }}
|
||||
file_input: |
|
||||
commits: ./commits.txt
|
||||
# Runs the CLI without AGENTS.md as instructions, without the GitHub
|
||||
# MCP server and without auto-update; see setup-copilot-cli.
|
||||
copilot-cli-path: ${{ steps.copilot.outputs.launcher }}
|
||||
env:
|
||||
# The Copilot CLI reads its credential from the environment; the
|
||||
# workflow token carries it under the `copilot-requests` permission
|
||||
# granted above, so no PAT is needed.
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
AI_API_KEY: ${{ secrets.OPENROUTER_API_KEY }}
|
||||
TAG: ${{ steps.tag.outputs.tag }}
|
||||
run: |
|
||||
bash .github/scripts/render-prompt.sh .github/prompts/telegram-release-summary.prompt.yml \
|
||||
"$TAG" commits.txt "$RUNNER_TEMP/system.txt" "$RUNNER_TEMP/user.txt"
|
||||
bash .github/scripts/ai-infer.sh "$RUNNER_TEMP/system.txt" "$RUNNER_TEMP/user.txt" "$RUNNER_TEMP/summary.txt"
|
||||
|
||||
- name: Post release announcement to Telegram
|
||||
if: steps.gate.outputs.skip != 'true'
|
||||
@@ -159,20 +142,13 @@ jobs:
|
||||
TELEGRAM_CHAT_ID: ${{ secrets.TELEGRAM_CHAT_ID }}
|
||||
TAG: ${{ steps.tag.outputs.tag }}
|
||||
REPO: ${{ github.repository }}
|
||||
AI_RESPONSE_FILE: ${{ steps.ai.outputs.response-file }}
|
||||
AI_RESPONSE: ${{ steps.ai.outputs.response }}
|
||||
run: |
|
||||
if [ -z "$TELEGRAM_BOT_TOKEN" ] || [ -z "$TELEGRAM_CHAT_ID" ]; then
|
||||
echo "::warning::TELEGRAM_BOT_TOKEN or TELEGRAM_CHAT_ID is not set — skipping Telegram notification."
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# Prefer the file output — `response` can be truncated for longer summaries.
|
||||
if [ -n "$AI_RESPONSE_FILE" ] && [ -f "$AI_RESPONSE_FILE" ]; then
|
||||
SUMMARY=$(cat "$AI_RESPONSE_FILE")
|
||||
else
|
||||
SUMMARY="$AI_RESPONSE"
|
||||
fi
|
||||
SUMMARY=$(cat "$RUNNER_TEMP/summary.txt")
|
||||
|
||||
if [ -z "${SUMMARY//[[:space:]]/}" ]; then
|
||||
echo "::error::AI summary is empty"
|
||||
|
||||
@@ -7,9 +7,6 @@ on:
|
||||
permissions:
|
||||
contents: read
|
||||
pull-requests: write
|
||||
# GitHub Models (models.github.ai) was retired on 2026-07-30; the workflow
|
||||
# token reaches GitHub Copilot through this permission instead.
|
||||
copilot-requests: write
|
||||
|
||||
# `edited` and `synchronize` can arrive back to back. Queue rather than cancel,
|
||||
# so a run is never stopped between commenting and closing.
|
||||
@@ -17,11 +14,6 @@ concurrency:
|
||||
group: pr-ai-policy-${{ github.event.pull_request.number }}
|
||||
cancel-in-progress: false
|
||||
|
||||
env:
|
||||
# `auto` lets Copilot pick. It is the only value every Copilot plan accepts
|
||||
# and it cannot go stale the way the pinned `openai/gpt-4.1` did.
|
||||
COPILOT_MODEL: auto
|
||||
|
||||
jobs:
|
||||
check-ai-policy:
|
||||
# Nobody is exempt: maintainers are checked like everyone else. The one
|
||||
@@ -105,15 +97,9 @@ jobs:
|
||||
{"compliant": true, "violations": []}
|
||||
PROMPT
|
||||
|
||||
# continue-on-error: a failed install must not stop the deterministic
|
||||
# trailer scan from acting. The Copilot step then records the outage.
|
||||
- name: Set up Copilot CLI
|
||||
continue-on-error: true
|
||||
uses: ./.github/actions/setup-copilot-cli
|
||||
|
||||
- name: Ask Copilot
|
||||
- name: Ask the model
|
||||
env:
|
||||
COPILOT_GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
AI_API_KEY: ${{ secrets.OPENROUTER_API_KEY }}
|
||||
TRAILER_HIT: ${{ steps.trailers.outputs.hit }}
|
||||
run: |
|
||||
{
|
||||
@@ -128,15 +114,15 @@ jobs:
|
||||
# The model half of this check is fail-open: an outage or an answer
|
||||
# that is not JSON must never close a pull request. The deterministic
|
||||
# trailer scan still stands on its own below.
|
||||
if ! bash .github/scripts/copilot-infer.sh /tmp/system.txt /tmp/user.txt /tmp/raw.txt; then
|
||||
printf '%s\n' "the Copilot call failed" >> /tmp/ai-degraded
|
||||
if ! bash .github/scripts/ai-infer.sh /tmp/system.txt /tmp/user.txt /tmp/raw.txt; then
|
||||
printf '%s\n' "the model call failed" >> /tmp/ai-degraded
|
||||
echo '{"compliant": true, "violations": []}' > /tmp/result.json
|
||||
exit 0
|
||||
fi
|
||||
|
||||
if ! python3 .github/scripts/extract-json.py /tmp/raw.txt /tmp/result.json; then
|
||||
echo "::error::Copilot returned no JSON object; treating as compliant"
|
||||
printf '%s\n' "Copilot returned output that was not JSON" >> /tmp/ai-degraded
|
||||
echo "::error::The model returned no JSON object; treating as compliant"
|
||||
printf '%s\n' "the model returned output that was not JSON" >> /tmp/ai-degraded
|
||||
echo '{"compliant": true, "violations": []}' > /tmp/result.json
|
||||
fi
|
||||
echo "Policy response validated"
|
||||
|
||||
@@ -8,7 +8,6 @@ on:
|
||||
|
||||
permissions:
|
||||
contents: write
|
||||
copilot-requests: write
|
||||
|
||||
jobs:
|
||||
generate-release-notes:
|
||||
@@ -79,44 +78,24 @@ jobs:
|
||||
git log --pretty=format:"- %s (%h by %an)" "$PREVIOUS_TAG".."$CURRENT_TAG" --no-merges > commits.txt
|
||||
echo "Collected $(wc -l < commits.txt) commits"
|
||||
|
||||
- name: Set up Copilot CLI
|
||||
id: copilot
|
||||
if: steps.get-release.outputs.is-prerelease == 'false'
|
||||
uses: ./.github/actions/setup-copilot-cli
|
||||
|
||||
- name: Generate release notes with AI
|
||||
id: generate-notes
|
||||
if: steps.get-release.outputs.is-prerelease == 'false'
|
||||
uses: actions/ai-inference@2c43c91ae16266ca159d311430343c67a5ffa222 # v3
|
||||
with:
|
||||
prompt-file: .github/prompts/release-notes.prompt.yml
|
||||
input: |
|
||||
version: ${{ steps.get-previous-tag.outputs.current-tag }}
|
||||
file_input: |
|
||||
commits: ./commits.txt
|
||||
# Runs the CLI without AGENTS.md as instructions, without the GitHub
|
||||
# MCP server and without auto-update; see setup-copilot-cli.
|
||||
copilot-cli-path: ${{ steps.copilot.outputs.launcher }}
|
||||
env:
|
||||
# The Copilot CLI reads its credential from the environment; the
|
||||
# workflow token carries it under the `copilot-requests` permission
|
||||
# granted above, so no PAT is needed.
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
AI_API_KEY: ${{ secrets.OPENROUTER_API_KEY }}
|
||||
VERSION: ${{ steps.get-previous-tag.outputs.current-tag }}
|
||||
run: |
|
||||
bash .github/scripts/render-prompt.sh .github/prompts/release-notes.prompt.yml \
|
||||
"$VERSION" commits.txt "$RUNNER_TEMP/system.txt" "$RUNNER_TEMP/user.txt"
|
||||
bash .github/scripts/ai-infer.sh "$RUNNER_TEMP/system.txt" "$RUNNER_TEMP/user.txt" "$RUNNER_TEMP/notes.txt"
|
||||
|
||||
- name: Update release with generated notes
|
||||
if: steps.get-release.outputs.is-prerelease == 'false'
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
RESPONSE_FILE: ${{ steps.generate-notes.outputs.response-file }}
|
||||
RESPONSE_OUTPUT: ${{ steps.generate-notes.outputs.response }}
|
||||
RELEASE_ID: ${{ steps.get-release.outputs.release-id }}
|
||||
run: |
|
||||
# Prefer reading from the response file to avoid output truncation
|
||||
if [ -n "$RESPONSE_FILE" ] && [ -f "$RESPONSE_FILE" ]; then
|
||||
RELEASE_NOTES=$(cat "$RESPONSE_FILE")
|
||||
else
|
||||
RELEASE_NOTES="$RESPONSE_OUTPUT"
|
||||
fi
|
||||
RELEASE_NOTES=$(cat "$RUNNER_TEMP/notes.txt")
|
||||
|
||||
# An empty answer would PATCH the release body to nothing and wipe
|
||||
# the notes the release already has.
|
||||
|
||||
Reference in new issue
Block a user