mirror of
https://github.com/iflytek/skillhub.git
synced 2026-09-15 23:31:10 +00:00
Merge branch 'main' of github.com:riverfor/skillhub
This commit is contained in:
commit
27443315ff
8 changed files with 616 additions and 0 deletions
42
.github/release-template.md
vendored
Normal file
42
.github/release-template.md
vendored
Normal file
|
|
@ -0,0 +1,42 @@
|
|||
# SkillHub {{version}}
|
||||
|
||||
{{One-line summary of the key changes in this release}}
|
||||
|
||||
## 🌟 Highlights
|
||||
|
||||
- {{Highlight 1}}
|
||||
- {{Highlight 2}}
|
||||
- {{Highlight 3}}
|
||||
|
||||
## 🚨 Breaking Changes
|
||||
|
||||
⚠️ {{If any, describe impact and migration guide}}
|
||||
|
||||
## ✨ Features
|
||||
|
||||
- {{Feature description}} by @author in #PR
|
||||
|
||||
## 🐛 Bug Fixes
|
||||
|
||||
- {{Fix description}} by @author in #PR
|
||||
|
||||
## ⚡ Performance
|
||||
|
||||
- {{Performance improvement}} by @author in #PR
|
||||
|
||||
## 📚 Documentation
|
||||
|
||||
- {{Documentation changes}} by @author in #PR
|
||||
|
||||
## 🔧 Chore
|
||||
|
||||
- {{Maintenance work}} by @author in #PR
|
||||
|
||||
## 📖 Documentation
|
||||
- Docs site: https://iflytek.github.io/skillhub/
|
||||
|
||||
## 👥 New Contributors
|
||||
{{Keep as-is}}
|
||||
|
||||
**Full Changelog**: https://github.com/iflytek/skillhub/compare/{{prev_tag}}...{{tag}}
|
||||
|
||||
41
.github/scripts/github.ts
vendored
41
.github/scripts/github.ts
vendored
|
|
@ -138,6 +138,47 @@ export class GitHubClient {
|
|||
);
|
||||
}
|
||||
|
||||
async listCommitPulls(sha: string): Promise<Array<{ number: number; title: string }>> {
|
||||
return this.request(
|
||||
"GET",
|
||||
`/repos/${this.owner}/${this.repo}/commits/${sha}/pulls`,
|
||||
);
|
||||
}
|
||||
|
||||
async createDraftRelease(
|
||||
tag: string,
|
||||
name: string,
|
||||
body: string,
|
||||
): Promise<{ id: number; upload_url: string }> {
|
||||
return this.request("POST", `/repos/${this.owner}/${this.repo}/releases`, {
|
||||
tag_name: tag,
|
||||
name,
|
||||
body,
|
||||
draft: true,
|
||||
prerelease: false,
|
||||
});
|
||||
}
|
||||
|
||||
async uploadReleaseAsset(
|
||||
releaseId: number,
|
||||
filename: string,
|
||||
content: string,
|
||||
): Promise<void> {
|
||||
const uploadUrl = `https://uploads.github.com/repos/${this.owner}/${this.repo}/releases/${releaseId}/assets?name=${encodeURIComponent(filename)}`;
|
||||
const response = await fetch(uploadUrl, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
...this.headers(),
|
||||
"Content-Type": "text/markdown",
|
||||
},
|
||||
body: content,
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw await GitHubApiError.fromResponse(response);
|
||||
}
|
||||
}
|
||||
|
||||
private async paginate<T>(path: string): Promise<T[]> {
|
||||
const collected: T[] = [];
|
||||
let nextPath: string | null = path;
|
||||
|
|
|
|||
80
.github/scripts/issue-llm-provider.ts
vendored
80
.github/scripts/issue-llm-provider.ts
vendored
|
|
@ -37,6 +37,35 @@ export async function requestOpenAiCompatibleJson(
|
|||
throw lastError ?? new Error("LLM request failed for an unknown reason.");
|
||||
}
|
||||
|
||||
export async function requestOpenAiCompatibleMarkdown(
|
||||
config: IssueLlmConfig,
|
||||
systemPrompt: string,
|
||||
userPrompt: string,
|
||||
): Promise<string> {
|
||||
let lastError: Error | null = null;
|
||||
|
||||
for (let attempt = 1; attempt <= config.maxAttempts; attempt += 1) {
|
||||
try {
|
||||
return await requestOnceMarkdown(config, systemPrompt, userPrompt);
|
||||
} catch (error) {
|
||||
const normalized = normalizeRequestError(
|
||||
error,
|
||||
attempt,
|
||||
config.maxAttempts,
|
||||
);
|
||||
lastError = normalized;
|
||||
|
||||
if (!shouldRetry(error) || attempt >= config.maxAttempts) {
|
||||
throw normalized;
|
||||
}
|
||||
|
||||
await sleep(resolveRetryDelay(error, config.retryBackoffMs, attempt));
|
||||
}
|
||||
}
|
||||
|
||||
throw lastError ?? new Error("LLM request failed for an unknown reason.");
|
||||
}
|
||||
|
||||
async function requestOnce(
|
||||
config: IssueLlmConfig,
|
||||
systemPrompt: string,
|
||||
|
|
@ -88,6 +117,57 @@ async function requestOnce(
|
|||
}
|
||||
}
|
||||
|
||||
async function requestOnceMarkdown(
|
||||
config: IssueLlmConfig,
|
||||
systemPrompt: string,
|
||||
userPrompt: string,
|
||||
): Promise<string> {
|
||||
const controller = new AbortController();
|
||||
const timeout = setTimeout(() => controller.abort(), config.timeoutMs);
|
||||
|
||||
try {
|
||||
const response = await fetch(`${config.baseUrl}/chat/completions`, {
|
||||
method: "POST",
|
||||
signal: controller.signal,
|
||||
headers: {
|
||||
Authorization: `Bearer ${config.apiKey}`,
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify({
|
||||
model: config.model,
|
||||
temperature: config.temperature,
|
||||
messages: [
|
||||
{ role: "system", content: systemPrompt },
|
||||
{ role: "user", content: userPrompt },
|
||||
],
|
||||
}),
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
const message =
|
||||
`LLM request failed with status ${response.status}: ${await response
|
||||
.text()}`;
|
||||
throw new RetryableHttpError(
|
||||
message,
|
||||
response.status,
|
||||
response.headers.get("retry-after"),
|
||||
);
|
||||
}
|
||||
|
||||
const payload = (await response.json()) as OpenAiCompatibleResponse;
|
||||
const content = payload.choices?.[0]?.message?.content;
|
||||
const text = normalizeMessageContent(content);
|
||||
|
||||
if (!text) {
|
||||
throw new Error("LLM response did not include message content.");
|
||||
}
|
||||
|
||||
return text;
|
||||
} finally {
|
||||
clearTimeout(timeout);
|
||||
}
|
||||
}
|
||||
|
||||
class RetryableHttpError extends Error {
|
||||
status: number;
|
||||
retryAfterSeconds: number | null;
|
||||
|
|
|
|||
53
.github/scripts/release-notes-config.ts
vendored
Normal file
53
.github/scripts/release-notes-config.ts
vendored
Normal file
|
|
@ -0,0 +1,53 @@
|
|||
import { IssueLlmConfig } from "./issue-llm-types.ts";
|
||||
|
||||
const DEFAULT_TIMEOUT_MS = 30000;
|
||||
const DEFAULT_MAX_ATTEMPTS = 2;
|
||||
const DEFAULT_RETRY_BACKOFF_MS = 1500;
|
||||
const DEFAULT_TEMPERATURE = 0.2;
|
||||
|
||||
export function readReleaseNotesLlmConfig(): IssueLlmConfig | null {
|
||||
const baseUrl = normalizeUrl(
|
||||
Deno.env.get("RELEASE_NOTES_LLM_BASE_URL") ||
|
||||
Deno.env.get("ISSUE_TRIAGE_LLM_BASE_URL"),
|
||||
);
|
||||
const apiKey = (
|
||||
Deno.env.get("RELEASE_NOTES_LLM_API_KEY") ||
|
||||
Deno.env.get("ISSUE_TRIAGE_LLM_API_KEY")
|
||||
)?.trim() ?? "";
|
||||
const model = (
|
||||
Deno.env.get("RELEASE_NOTES_LLM_MODEL") ||
|
||||
Deno.env.get("ISSUE_TRIAGE_LLM_MODEL")
|
||||
)?.trim() ?? "";
|
||||
|
||||
if (!baseUrl || !apiKey || !model) {
|
||||
console.warn(
|
||||
"LLM config missing, will use fallback mode (conventional commit grouping)",
|
||||
);
|
||||
return null;
|
||||
}
|
||||
|
||||
return {
|
||||
mode: "assist",
|
||||
provider: "openai-compatible",
|
||||
baseUrl,
|
||||
apiKey,
|
||||
model,
|
||||
timeoutMs: DEFAULT_TIMEOUT_MS,
|
||||
maxAttempts: DEFAULT_MAX_ATTEMPTS,
|
||||
retryBackoffMs: DEFAULT_RETRY_BACKOFF_MS,
|
||||
temperature: DEFAULT_TEMPERATURE,
|
||||
maxComments: 0,
|
||||
maxCommentChars: 0,
|
||||
maxBodyChars: 0,
|
||||
};
|
||||
}
|
||||
|
||||
function normalizeUrl(value: string | undefined | null) {
|
||||
const trimmed = value?.trim();
|
||||
|
||||
if (!trimmed) {
|
||||
return "";
|
||||
}
|
||||
|
||||
return trimmed.endsWith("/") ? trimmed.slice(0, -1) : trimmed;
|
||||
}
|
||||
271
.github/scripts/release-notes-generator.ts
vendored
Normal file
271
.github/scripts/release-notes-generator.ts
vendored
Normal file
|
|
@ -0,0 +1,271 @@
|
|||
import { GitHubClient } from "./github.ts";
|
||||
import { IssueLlmConfig } from "./issue-llm-types.ts";
|
||||
import { requestOpenAiCompatibleMarkdown } from "./issue-llm-provider.ts";
|
||||
|
||||
interface RawCommit {
|
||||
sha: string;
|
||||
message: string;
|
||||
author: string;
|
||||
prNumber?: number;
|
||||
prTitle?: string;
|
||||
excluded: boolean;
|
||||
}
|
||||
|
||||
interface ChangeEntry {
|
||||
type: string;
|
||||
scope: string;
|
||||
description: string;
|
||||
prNumber?: number;
|
||||
authors: string[];
|
||||
commits: string[];
|
||||
}
|
||||
|
||||
export async function generateReleaseNotes(
|
||||
owner: string,
|
||||
repo: string,
|
||||
tag: string,
|
||||
prevTag: string,
|
||||
llmConfig: IssueLlmConfig | null,
|
||||
dryRun: boolean,
|
||||
): Promise<string> {
|
||||
const github = new GitHubClient(Deno.env.get("GH_TOKEN") ?? "", owner, repo);
|
||||
|
||||
console.log(`Collecting changes from ${prevTag} to ${tag}...`);
|
||||
const changes = await collectChanges(github, prevTag, tag);
|
||||
console.log(`Found ${changes.length} changes after deduplication`);
|
||||
|
||||
if (llmConfig) {
|
||||
console.log("Generating release notes with LLM...");
|
||||
try {
|
||||
const markdown = await generateMarkdownWithLLM(
|
||||
llmConfig,
|
||||
changes,
|
||||
tag,
|
||||
prevTag,
|
||||
owner,
|
||||
repo,
|
||||
);
|
||||
return markdown;
|
||||
} catch (error) {
|
||||
console.error("LLM generation failed:", error);
|
||||
console.log("Falling back to conventional commit grouping...");
|
||||
return generateFallback(changes, tag, prevTag, owner, repo);
|
||||
}
|
||||
} else {
|
||||
console.log("Using fallback mode (conventional commit grouping)...");
|
||||
return generateFallback(changes, tag, prevTag, owner, repo);
|
||||
}
|
||||
}
|
||||
|
||||
async function collectChanges(
|
||||
github: GitHubClient,
|
||||
prevTag: string,
|
||||
tag: string,
|
||||
): Promise<ChangeEntry[]> {
|
||||
const gitLogCmd = new Deno.Command("git", {
|
||||
args: ["log", `${prevTag}..${tag}`, "--format=%H|%s|%an"],
|
||||
stdout: "piped",
|
||||
});
|
||||
const gitLogOutput = await gitLogCmd.output();
|
||||
const gitLogText = new TextDecoder().decode(gitLogOutput.stdout);
|
||||
|
||||
const rawCommits: RawCommit[] = [];
|
||||
for (const line of gitLogText.trim().split("\n")) {
|
||||
if (!line) continue;
|
||||
const [sha, message, author] = line.split("|");
|
||||
rawCommits.push({
|
||||
sha,
|
||||
message,
|
||||
author,
|
||||
excluded: false,
|
||||
});
|
||||
}
|
||||
|
||||
for (const commit of rawCommits) {
|
||||
if (/^Revert "(.+)"$/.test(commit.message)) {
|
||||
commit.excluded = true;
|
||||
const revertedMsg = commit.message.match(/^Revert "(.+)"$/)?.[1];
|
||||
if (revertedMsg) {
|
||||
const reverted = rawCommits.find((c) => c.message === revertedMsg);
|
||||
if (reverted) reverted.excluded = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (/^Merge (pull request|branch|remote-tracking)/.test(commit.message)) {
|
||||
commit.excluded = true;
|
||||
}
|
||||
}
|
||||
|
||||
for (const commit of rawCommits.filter((c) => !c.excluded)) {
|
||||
try {
|
||||
const pulls = await github.listCommitPulls(commit.sha);
|
||||
if (pulls.length > 0) {
|
||||
commit.prNumber = pulls[0].number;
|
||||
commit.prTitle = pulls[0].title;
|
||||
}
|
||||
} catch (error) {
|
||||
console.warn(`Failed to fetch PR for commit ${commit.sha}:`, error);
|
||||
}
|
||||
}
|
||||
|
||||
const prMap = new Map<number, ChangeEntry>();
|
||||
const standaloneCommits: ChangeEntry[] = [];
|
||||
|
||||
for (const commit of rawCommits.filter((c) => !c.excluded)) {
|
||||
const parsed = parseConventionalCommit(commit.message);
|
||||
const description = commit.prTitle || parsed.description;
|
||||
|
||||
if (commit.prNumber) {
|
||||
if (!prMap.has(commit.prNumber)) {
|
||||
prMap.set(commit.prNumber, {
|
||||
type: parsed.type,
|
||||
scope: parsed.scope,
|
||||
description,
|
||||
prNumber: commit.prNumber,
|
||||
authors: [commit.author],
|
||||
commits: [commit.sha],
|
||||
});
|
||||
} else {
|
||||
const entry = prMap.get(commit.prNumber)!;
|
||||
if (!entry.authors.includes(commit.author)) {
|
||||
entry.authors.push(commit.author);
|
||||
}
|
||||
entry.commits.push(commit.sha);
|
||||
}
|
||||
} else {
|
||||
standaloneCommits.push({
|
||||
type: parsed.type,
|
||||
scope: parsed.scope,
|
||||
description,
|
||||
authors: [commit.author],
|
||||
commits: [commit.sha],
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return [...prMap.values(), ...standaloneCommits];
|
||||
}
|
||||
|
||||
function parseConventionalCommit(message: string): {
|
||||
type: string;
|
||||
scope: string;
|
||||
description: string;
|
||||
} {
|
||||
const match = message.match(/^(\w+)(?:\(([^)]+)\))?: (.+)$/);
|
||||
if (match) {
|
||||
return {
|
||||
type: match[1],
|
||||
scope: match[2] || "",
|
||||
description: match[3],
|
||||
};
|
||||
}
|
||||
return {
|
||||
type: "other",
|
||||
scope: "",
|
||||
description: message,
|
||||
};
|
||||
}
|
||||
|
||||
async function generateMarkdownWithLLM(
|
||||
config: IssueLlmConfig,
|
||||
changes: ChangeEntry[],
|
||||
tag: string,
|
||||
prevTag: string,
|
||||
owner: string,
|
||||
repo: string,
|
||||
): Promise<string> {
|
||||
const template = await Deno.readTextFile(".github/release-template.md");
|
||||
|
||||
const systemPrompt = `You are a senior product manager and technical documentation expert. Rewrite the following technical change list into user-friendly Release Notes.
|
||||
|
||||
Requirements:
|
||||
1. Strictly follow the Markdown structure and heading levels of the template below
|
||||
2. Remove any section entirely (including its heading) if there are no items for it
|
||||
3. Rewrite technical jargon into language that end-users can understand
|
||||
4. For Breaking Changes, add an upgrade / migration guide
|
||||
5. Highlights must contain 2-4 items, distilled from the most important changes
|
||||
6. PR number format: #123
|
||||
7. Contributor format: @username
|
||||
8. Replace {{version}}, {{prev_tag}}, {{tag}} with actual values
|
||||
9. Output ONLY the Markdown content — no extra commentary, no code fences
|
||||
|
||||
Template:
|
||||
---
|
||||
${template}
|
||||
---`;
|
||||
|
||||
const changesList = changes.map((c) => {
|
||||
const pr = c.prNumber ? ` (#${c.prNumber})` : "";
|
||||
const authors = c.authors.map((a) => `@${a}`).join(", ");
|
||||
return `- ${c.type}(${c.scope}): ${c.description}${pr} by ${authors}`;
|
||||
}).join("\n");
|
||||
|
||||
const userPrompt = `Version: ${tag}
|
||||
Previous version: ${prevTag}
|
||||
Repository: ${owner}/${repo}
|
||||
|
||||
Change list:
|
||||
${changesList}`;
|
||||
|
||||
const markdown = await requestOpenAiCompatibleMarkdown(
|
||||
config,
|
||||
systemPrompt,
|
||||
userPrompt,
|
||||
);
|
||||
|
||||
return markdown
|
||||
.replace(/\{\{version\}\}/g, tag)
|
||||
.replace(/\{\{prev_tag\}\}/g, prevTag)
|
||||
.replace(/\{\{tag\}\}/g, tag);
|
||||
}
|
||||
|
||||
function generateFallback(
|
||||
changes: ChangeEntry[],
|
||||
tag: string,
|
||||
prevTag: string,
|
||||
owner: string,
|
||||
repo: string,
|
||||
): string {
|
||||
const grouped = new Map<string, ChangeEntry[]>();
|
||||
for (const change of changes) {
|
||||
const type = change.type;
|
||||
if (!grouped.has(type)) {
|
||||
grouped.set(type, []);
|
||||
}
|
||||
grouped.get(type)!.push(change);
|
||||
}
|
||||
|
||||
const typeLabels: Record<string, string> = {
|
||||
feat: "## ✨ Features",
|
||||
fix: "## 🐛 Bug Fixes",
|
||||
docs: "## 📚 Documentation",
|
||||
perf: "## ⚡ Performance",
|
||||
refactor: "## 🔧 Improvements",
|
||||
test: "## 🧪 Tests",
|
||||
chore: "## 🔧 Chore",
|
||||
};
|
||||
|
||||
let md = `# SkillHub ${tag}\n\n`;
|
||||
md += `> [Auto-generated - LLM unavailable]\n\n`;
|
||||
|
||||
for (const [type, items] of grouped.entries()) {
|
||||
const label = typeLabels[type] || `## ${type}`;
|
||||
md += `${label}\n\n`;
|
||||
for (const item of items) {
|
||||
const pr = item.prNumber ? ` in #${item.prNumber}` : "";
|
||||
const authors = item.authors.map((a) => `@${a}`).join(", ");
|
||||
md += `- ${item.description}${pr} by ${authors}\n`;
|
||||
}
|
||||
md += "\n";
|
||||
}
|
||||
|
||||
const contributors = [
|
||||
...new Set(changes.flatMap((c) => c.authors)),
|
||||
];
|
||||
md += `## 👥 Contributors\n\n`;
|
||||
md += contributors.map((a) => `@${a}`).join(", ") + "\n\n";
|
||||
|
||||
md += `**Full Changelog**: https://github.com/${owner}/${repo}/compare/${prevTag}...${tag}\n`;
|
||||
|
||||
return md;
|
||||
}
|
||||
79
.github/scripts/release-notes.ts
vendored
Normal file
79
.github/scripts/release-notes.ts
vendored
Normal file
|
|
@ -0,0 +1,79 @@
|
|||
import { GitHubClient } from "./github.ts";
|
||||
import { readReleaseNotesLlmConfig } from "./release-notes-config.ts";
|
||||
import { generateReleaseNotes } from "./release-notes-generator.ts";
|
||||
|
||||
function readFlag(name: string): string | null {
|
||||
const index = Deno.args.indexOf(name);
|
||||
if (index === -1 || index === Deno.args.length - 1) {
|
||||
return null;
|
||||
}
|
||||
return Deno.args[index + 1];
|
||||
}
|
||||
|
||||
function hasFlag(name: string): boolean {
|
||||
return Deno.args.includes(name);
|
||||
}
|
||||
|
||||
async function detectPrevTag(tag: string): Promise<string> {
|
||||
const cmd = new Deno.Command("git", {
|
||||
args: ["tag", "--sort=-v:refname"],
|
||||
stdout: "piped",
|
||||
});
|
||||
const output = await cmd.output();
|
||||
const tags = new TextDecoder().decode(output.stdout).trim().split("\n");
|
||||
|
||||
const currentIndex = tags.indexOf(tag);
|
||||
if (currentIndex === -1 || currentIndex === tags.length - 1) {
|
||||
throw new Error(`Cannot find previous tag for ${tag}`);
|
||||
}
|
||||
|
||||
return tags[currentIndex + 1];
|
||||
}
|
||||
|
||||
async function main() {
|
||||
const owner = readFlag("--owner");
|
||||
const repo = readFlag("--repo");
|
||||
const tag = readFlag("--tag");
|
||||
const prevTagArg = readFlag("--prev-tag");
|
||||
const dryRun = hasFlag("--dry-run");
|
||||
const skipLlm = hasFlag("--skip-llm");
|
||||
|
||||
if (!owner || !repo || !tag) {
|
||||
console.error("Usage: release-notes.ts --owner <owner> --repo <repo> --tag <tag> [--prev-tag <prev-tag>] [--dry-run] [--skip-llm]");
|
||||
Deno.exit(1);
|
||||
}
|
||||
|
||||
const prevTag = prevTagArg || await detectPrevTag(tag);
|
||||
console.log(`Generating release notes for ${tag} (previous: ${prevTag})`);
|
||||
|
||||
const llmConfig = skipLlm ? null : readReleaseNotesLlmConfig();
|
||||
|
||||
const markdown = await generateReleaseNotes(
|
||||
owner,
|
||||
repo,
|
||||
tag,
|
||||
prevTag,
|
||||
llmConfig,
|
||||
dryRun,
|
||||
);
|
||||
|
||||
if (dryRun) {
|
||||
console.log("\n=== DRY RUN MODE ===\n");
|
||||
console.log(markdown);
|
||||
console.log("\n=== END DRY RUN ===");
|
||||
return;
|
||||
}
|
||||
|
||||
console.log("Creating draft release...");
|
||||
const github = new GitHubClient(Deno.env.get("GH_TOKEN") ?? "", owner, repo);
|
||||
const release = await github.createDraftRelease(tag, tag, markdown);
|
||||
console.log(`Draft release created: ${release.id}`);
|
||||
|
||||
console.log(`\nDraft release created successfully!`);
|
||||
console.log(`View at: https://github.com/${owner}/${repo}/releases/tag/${tag}`);
|
||||
}
|
||||
|
||||
main().catch((error) => {
|
||||
console.error("Error:", error);
|
||||
Deno.exit(1);
|
||||
});
|
||||
46
.github/workflows/release-notes.yml
vendored
Normal file
46
.github/workflows/release-notes.yml
vendored
Normal file
|
|
@ -0,0 +1,46 @@
|
|||
name: AI Release Notes
|
||||
|
||||
on:
|
||||
push:
|
||||
tags: ["v*"]
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
tag:
|
||||
description: "Tag to generate release notes for (e.g. v0.3.0)"
|
||||
required: true
|
||||
prev_tag:
|
||||
description: "Previous tag (auto-detect if empty)"
|
||||
required: false
|
||||
|
||||
permissions:
|
||||
contents: write
|
||||
|
||||
jobs:
|
||||
generate:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- uses: denoland/setup-deno@667a34cdef165d8d2b2e98dde39547c9daac7282 # v2.0.4
|
||||
with:
|
||||
deno-version: v2.x
|
||||
|
||||
- name: Generate release notes
|
||||
env:
|
||||
GH_TOKEN: ${{ github.token }}
|
||||
RELEASE_NOTES_LLM_BASE_URL: ${{ vars.RELEASE_NOTES_LLM_BASE_URL || 'https://models.inference.ai.azure.com' }}
|
||||
RELEASE_NOTES_LLM_API_KEY: ${{ secrets.RELEASE_NOTES_LLM_API_KEY || github.token }}
|
||||
RELEASE_NOTES_LLM_MODEL: ${{ vars.RELEASE_NOTES_LLM_MODEL || 'gpt-4o-mini' }}
|
||||
ISSUE_TRIAGE_LLM_BASE_URL: ${{ vars.ISSUE_TRIAGE_LLM_BASE_URL }}
|
||||
ISSUE_TRIAGE_LLM_API_KEY: ${{ secrets.ISSUE_TRIAGE_LLM_API_KEY }}
|
||||
ISSUE_TRIAGE_LLM_MODEL: ${{ vars.ISSUE_TRIAGE_LLM_MODEL }}
|
||||
run: |
|
||||
TAG="${{ github.event.inputs.tag || github.ref_name }}"
|
||||
PREV="${{ github.event.inputs.prev_tag }}"
|
||||
ARGS="--owner ${{ github.repository_owner }} --repo ${{ github.event.repository.name }} --tag $TAG"
|
||||
[ -n "$PREV" ] && ARGS="$ARGS --prev-tag $PREV"
|
||||
|
||||
deno run --allow-env --allow-net --allow-run --allow-read \
|
||||
.github/scripts/release-notes.ts $ARGS
|
||||
4
.gitignore
vendored
4
.gitignore
vendored
|
|
@ -62,6 +62,7 @@ package-lock.json
|
|||
**/.playwright/
|
||||
**/playwright-report/
|
||||
**/test-results/
|
||||
.playwright-mcp/
|
||||
|
||||
# Python / temporary files
|
||||
*.py[cod]
|
||||
|
|
@ -82,3 +83,6 @@ docs/superpowers/
|
|||
# Local workspace metadata
|
||||
AGENTS.md
|
||||
CLAUDE.md
|
||||
|
||||
# Local config file
|
||||
.mcp.json
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue