Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions .changeset/fix-cf-asset-cache-headers.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
---
'@astrojs/cloudflare': minor
---

Sets immutable cache headers for static assets

Static assets under `_astro` can be cached to improve performance. The adapter now automatically injects a `Cache-Control` header at build time when possible.
2 changes: 0 additions & 2 deletions .flue/agents/merge-resolve.ts

This file was deleted.

23 changes: 23 additions & 0 deletions .flue/lib/github.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
import { exec as execCb } from 'node:child_process';
import { promisify } from 'node:util';
import * as v from 'valibot';

const execAsync = promisify(execCb);

const REPO = process.env.GITHUB_REPOSITORY || 'withastro/astro';
export const GITHUB_TOKEN_BASE = process.env.GITHUB_TOKEN;

Expand Down Expand Up @@ -207,3 +211,22 @@ export async function removeGitHubLabel(issueNumber: number, label: string): Pro
throw new Error(`Failed to remove label (HTTP ${res.status}): ${await res.text()}`);
}
}

/**
* Push a branch to origin using the privileged token. Runs outside the sandbox
* so the agent never sees the write-capable token.
*/
export async function gitPush(
branch: string,
options?: { force?: boolean },
): Promise<{ exitCode: number; stdout: string; stderr: string }> {
assert(GITHUB_TOKEN_PRIVILEGED, 'FREDKBOT_GITHUB_TOKEN token is required.');
const forceFlag = options?.force ? ' -f' : '';
const remoteUrl = `https://x-access-token:${GITHUB_TOKEN_PRIVILEGED}@github.com/${REPO}.git`;
try {
const { stdout, stderr } = await execAsync(`git push${forceFlag} ${remoteUrl} ${branch}`);
return { exitCode: 0, stdout, stderr };
} catch (err: any) {
return { exitCode: err.code ?? 1, stdout: err.stdout ?? '', stderr: err.stderr ?? '' };
}
}
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import type { FlueContext } from '@flue/sdk/client';
import { defineCommand } from '@flue/sdk/node';
import { createAgent, type FlueContext } from '@flue/runtime';
import { local } from '@flue/runtime/node';
import * as v from 'valibot';
import {
GITHUB_TOKEN_BASE,
Expand All @@ -11,23 +11,21 @@ import {
removeGitHubLabel,
} from '../lib/github.ts';

// CLI-only agent: no HTTP trigger. Invoked from GitHub Actions via `flue run fix-verification`.
export const triggers = {};

const gh = defineCommand('gh', { env: { GH_TOKEN: GITHUB_TOKEN_BASE } });
const git = defineCommand('git');
const node = defineCommand('node');
const pnpm = defineCommand('pnpm');
const agent = createAgent(() => ({
sandbox: local({
env: {
GH_TOKEN: GITHUB_TOKEN_BASE,
},
}),
model: 'anthropic/claude-sonnet-4-20250514',
}));

export default async function ({ init, payload }: FlueContext) {
export async function run({ init, payload }: FlueContext) {
const issueNumber = payload.issueNumber as number;
const branch = `flue/fix-${issueNumber}`;

const agent = await init({
sandbox: 'local',
model: 'anthropic/claude-sonnet-4-20250514',
});
const session = await agent.session();
const harness = await init(agent);
const session = await harness.session();

const issueDetails = await fetchIssueDetails(issueNumber);

Expand All @@ -42,7 +40,7 @@ export default async function ({ init, payload }: FlueContext) {
}

// Ask the LLM whether this comment confirms the fix works.
const classification = await session.prompt(
const { data: classification } = await session.prompt(
`You are reviewing a GitHub issue comment to determine if the commenter is confirming that a proposed fix works.

## Context
Expand Down Expand Up @@ -119,7 +117,7 @@ Return your classification.`,
}

// Use the astro-pr-writer skill to generate a good PR title and body.
const prContent = await session.skill('astro-pr-writer/SKILL.md', {
const { data: prContent } = await session.skill('astro-pr-writer/SKILL.md', {
args: {
issueNumber,
issueDetails,
Expand All @@ -131,7 +129,6 @@ Generate a PR title and body following the astro-pr-writer conventions.
The PR should reference the issue with "Closes #${issueNumber}".
Do NOT create the PR yourself — just return the title and body content.`,
},
commands: [gh, git, node, pnpm],
result: v.object({
title: v.pipe(
v.string(),
Expand Down
96 changes: 38 additions & 58 deletions .flue/agents/issue-triage.ts → .flue/workflows/issue-triage.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import type { FlueContext, FlueSession } from '@flue/sdk/client';
import { defineCommand } from '@flue/sdk/node';
import { createAgent, type FlueContext, type FlueSession } from '@flue/runtime';
import { local } from '@flue/runtime/node';
import * as v from 'valibot';
import {
GITHUB_TOKEN_BASE,
Expand All @@ -8,44 +8,37 @@ import {
addGitHubLabels,
fetchIssueDetails,
fetchRepoLabels,
gitPush,
postGitHubComment,
removeGitHubLabel,
} from '../lib/github.ts';

// CLI-only agent: no HTTP trigger. Invoked from GitHub Actions via `flue run issue-triage`.
export const triggers = {};

// Define commands that are allowed as pass-through to the local GH Actions container.
const bgproc = defineCommand('bgproc');
const agentBrowser = defineCommand('agent-browser');
const node = defineCommand('node');
const npx = defineCommand('npx');
const pnpm = defineCommand('pnpm');
// pnpm variant with GitHub Actions env vars forwarded. pkg-pr-new checks these
// to verify it's running inside CI before publishing preview releases.
const pnpmCI = defineCommand('pnpm', {
env: {
GITHUB_ACTIONS: process.env.GITHUB_ACTIONS,
GITHUB_REPOSITORY: process.env.GITHUB_REPOSITORY,
GITHUB_RUN_ID: process.env.GITHUB_RUN_ID,
GITHUB_RUN_ATTEMPT: process.env.GITHUB_RUN_ATTEMPT,
GITHUB_ACTOR_ID: process.env.GITHUB_ACTOR_ID,
GITHUB_SHA: process.env.GITHUB_SHA,
GITHUB_REF_NAME: process.env.GITHUB_REF_NAME,
GITHUB_OUTPUT: process.env.GITHUB_OUTPUT,
GITHUB_EVENT_PATH: process.env.GITHUB_EVENT_PATH,
},
});
const gh = defineCommand('gh', { env: { GH_TOKEN: GITHUB_TOKEN_BASE } });
const git = defineCommand('git');
const gitWithAuth = defineCommand('git', { env: { GH_TOKEN: GITHUB_TOKEN_BASE } });
const agent = createAgent(() => ({
sandbox: local({
env: {
// Git/GitHub auth
GH_TOKEN: GITHUB_TOKEN_BASE,
// GitHub Actions env vars needed by pkg-pr-new for preview releases
GITHUB_ACTIONS: process.env.GITHUB_ACTIONS,
GITHUB_REPOSITORY: process.env.GITHUB_REPOSITORY,
GITHUB_RUN_ID: process.env.GITHUB_RUN_ID,
GITHUB_RUN_ATTEMPT: process.env.GITHUB_RUN_ATTEMPT,
GITHUB_ACTOR_ID: process.env.GITHUB_ACTOR_ID,
GITHUB_SHA: process.env.GITHUB_SHA,
GITHUB_REF_NAME: process.env.GITHUB_REF_NAME,
GITHUB_OUTPUT: process.env.GITHUB_OUTPUT,
GITHUB_EVENT_PATH: process.env.GITHUB_EVENT_PATH,
},
}),
model: 'anthropic/claude-opus-4-6',
}));

function assert(condition: unknown, message: string): asserts condition {
if (!condition) throw new Error(message);
}

async function shouldRetriage(session: FlueSession, issue: IssueDetails): Promise<'yes' | 'no'> {
return session.prompt(
const { data } = await session.prompt(
`You are reviewing a GitHub issue conversation to decide whether a triage re-run is warranted.

## Issue
Expand Down Expand Up @@ -74,6 +67,7 @@ meaningful reproduction information, respond with "no".
Return only "yes" or "no" inside the ---RESULT_START--- / ---RESULT_END--- block.`,
{ result: v.picklist(['yes', 'no']) },
);
return data;
}

async function selectTriageLabels(
Expand All @@ -87,7 +81,7 @@ async function selectTriageLabels(
const priorityLabelNames = priorityLabels.map((l) => l.name);
const packageLabelNames = packageLabels.map((l) => l.name);

const labelResult = await session.prompt(
const { data: labelResult } = await session.prompt(
`Label the following GitHub issue based on the triage report that was already posted.

Select labels for this issue from the lists below based on the triage report. Select exactly one priority label (the report's **Priority** section is a strong hint) and 0-3 package labels based on where the issue lives in the monorepo and how it manifests.
Expand Down Expand Up @@ -132,7 +126,7 @@ interface PreviewRelease {

async function publishPreviewRelease(session: FlueSession): Promise<PreviewRelease | null> {
// Determine which package directories were modified relative to main.
const diffResult = await session.shell('git diff main --name-only', { commands: [git] });
const diffResult = await session.shell('git diff main --name-only');
if (!diffResult.stdout.trim()) return null;

const changedFiles = diffResult.stdout.trim().split('\n');
Expand All @@ -149,7 +143,6 @@ async function publishPreviewRelease(session: FlueSession): Promise<PreviewRelea
const packages = [...packageDirs].join(' ');
const publishResult = await session.shell(
`pnpm dlx pkg-pr-new publish --pnpm --compact --no-template --comment=off --json preview-release.json ${packages}`,
{ commands: [pnpmCI] },
);

if (publishResult.exitCode !== 0) {
Expand All @@ -160,7 +153,6 @@ async function publishPreviewRelease(session: FlueSession): Promise<PreviewRelea
// Parse the JSON output to extract package URLs.
const jsonResult = await session.shell(
"node -e \"process.stdout.write(require('fs').readFileSync('preview-release.json','utf8'))\"",
{ commands: [node] },
);
try {
const output = JSON.parse(jsonResult.stdout.trim());
Expand All @@ -186,9 +178,8 @@ async function runTriagePipeline(
fixed: boolean;
commitMessage: string | null;
}> {
const reproduceResult = await session.skill('triage/reproduce.md', {
const { data: reproduceResult } = await session.skill('triage/reproduce.md', {
args: { issueNumber, issueDetails },
commands: [gh, bgproc, agentBrowser, git, node, npx, pnpm],
result: v.object({
reproducible: v.pipe(
v.boolean(),
Expand All @@ -215,19 +206,17 @@ async function runTriagePipeline(
};
}

const diagnoseResult = await session.skill('triage/diagnose.md', {
const { data: diagnoseResult } = await session.skill('triage/diagnose.md', {
args: { issueDetails },
commands: [gh, bgproc, agentBrowser, git, node, npx, pnpm],
result: v.object({
confidence: v.pipe(
v.nullable(v.picklist(['high', 'medium', 'low'])),
v.description('Diagnosis confidence level, null if not attempted'),
),
}),
});
const verifyResult = await session.skill('triage/verify.md', {
const { data: verifyResult } = await session.skill('triage/verify.md', {
args: { issueDetails },
commands: [gh, bgproc, agentBrowser, git, node, npx, pnpm],
result: v.object({
verdict: v.pipe(
v.picklist(['bug', 'intended-behavior', 'unclear']),
Expand All @@ -252,9 +241,8 @@ async function runTriagePipeline(
};
}

const fixResult = await session.skill('triage/fix.md', {
const { data: fixResult } = await session.skill('triage/fix.md', {
args: { issueDetails },
commands: [gh, bgproc, agentBrowser, git, node, npx, pnpm],
result: v.object({
fixed: v.pipe(
v.boolean(),
Expand All @@ -279,16 +267,12 @@ async function runTriagePipeline(
};
}

export default async function ({ init, payload }: FlueContext) {
export async function run({ init, payload }: FlueContext) {
const issueNumber = payload.issueNumber as number;
const branch = `flue/fix-${issueNumber}`;

// Initialize the agent and session.
const agent = await init({
sandbox: 'local',
model: 'anthropic/claude-opus-4-6',
});
const session = await agent.session();
const harness = await init(agent);
const session = await harness.session();

const issueDetails = await fetchIssueDetails(issueNumber);

Expand All @@ -312,22 +296,19 @@ export default async function ({ init, payload }: FlueContext) {
// - create a PR from that branch entirely in the GH UI
// - ignore it completely
{
const diff = await session.shell('git diff main --stat', { commands: [git] });
const diff = await session.shell('git diff main --stat');
if (diff.stdout.trim()) {
const status = await session.shell('git status --porcelain', { commands: [git] });
const status = await session.shell('git status --porcelain');
if (status.stdout.trim()) {
await session.shell('git add -A', { commands: [git] });
await session.shell('git add -A');
const defaultMessage = triageResult.fixed
? 'fix(auto-triage): automated fix'
: 'test(auto-triage): failing test and investigation notes';
await session.shell(
`git commit -m ${JSON.stringify(triageResult.commitMessage ?? defaultMessage)}`,
{ commands: [git] },
);
}
const pushResult = await session.shell(`git push -f origin ${branch}`, {
commands: [gitWithAuth],
});
const pushResult = await gitPush(branch, { force: true });
console.info('push result:', pushResult);
isPushed = pushResult.exitCode === 0;
}
Expand All @@ -353,9 +334,8 @@ export default async function ({ init, payload }: FlueContext) {
assert(packageLabels.length > 0, 'no package labels found');

const branchName = isPushed ? branch : null;
const comment = await session.skill('triage/comment.md', {
const { data: comment } = await session.skill('triage/comment.md', {
args: { branchName, priorityLabels, issueDetails, previewRelease },
commands: [gh, git, node, npx, pnpm],
result: v.pipe(
v.string(),
v.description(
Expand Down
1 change: 1 addition & 0 deletions .flue/workflows/merge-fix.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { args, run } from './merge-fix/WORKFLOW.ts';
Loading
Loading