Skip to content
Open
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
8 changes: 2 additions & 6 deletions .env.example
Original file line number Diff line number Diff line change
@@ -1,9 +1,5 @@
# GitHub Personal Access Token
# Generate at: https://github.com/settings/tokens
# Required scope: public_repo
# With authentication: 5000 requests/hour
# Without authentication: 60 requests/hour
NEXT_PUBLIC_GITHUB_TOKEN=
# GitHub data is fetched at build time, not in the browser. Run
# `GITHUB_TOKEN=$(gh auth token) yarn fetch-data` before `yarn dev`.

# Google Analytics Measurement ID
# Get your GA4 Measurement ID from: https://analytics.google.com
Expand Down
14 changes: 14 additions & 0 deletions .github/workflows/cd.yml
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,11 @@ name: CD
on:
push:
branches: [main]
# Refresh the GitHub data snapshot every hour (offset from the top of
# the hour, when scheduled runs are most often delayed). During the session,
# use "Run workflow" for an immediate refresh.
schedule:
- cron: '17 * * * *'
workflow_dispatch:

# Cancel in-progress deployments for same branch
Expand All @@ -19,8 +24,10 @@ permissions:

jobs:
# Wait for CI to complete before deploying
# Scheduled and manual runs rebuild an already-tested commit, so only pushes wait for CI
check-ci:
name: Wait for CI
if: github.event_name == 'push'
runs-on: ubuntu-latest
steps:
- name: Wait for CI workflow
Expand All @@ -34,6 +41,7 @@ jobs:
build:
name: Build
needs: check-ci
if: ${{ !cancelled() && (needs.check-ci.result == 'success' || needs.check-ci.result == 'skipped') }}
runs-on: ubuntu-latest
timeout-minutes: 15

Expand Down Expand Up @@ -67,6 +75,12 @@ jobs:
- name: Install dependencies
run: yarn install --immutable

# Fails the run on any GitHub error, so Pages keeps serving the last good deploy
- name: Fetch GitHub data snapshot
run: yarn fetch-data
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

- name: Build
run: yarn build

Expand Down
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,9 @@
# production
/build

# GitHub data snapshot (generated by `yarn fetch-data`)
/public/data/

# misc
.DS_Store
*.pem
Expand Down
16 changes: 14 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,13 +26,25 @@ cp .env.example .env.local
yarn install
```

4. Run the development server:
4. Fetch the GitHub data snapshot used by the issues and Contrib Champs pages:

```bash
GITHUB_TOKEN=$(gh auth token) yarn fetch-data

# Optionally add mock issues (see mocks/issues.example.json)
GITHUB_TOKEN=$(gh auth token) yarn fetch-data mocks/issues.example.json

# Or use only mock issues, with no GitHub token needed
yarn fetch-data --no-github mocks/issues.example.json
```

5. Run the development server:

```bash
yarn dev
```

Open [http://localhost:3000](http://localhost:3000) to view the application.
Open [http://localhost:3000](http://localhost:3000) to view the application. See [docs/deployment.md](docs/deployment.md) for how the snapshot is refreshed in production, and [docs/organizer-runbook.md](docs/organizer-runbook.md) for running an event.

## Build

Expand Down
16 changes: 15 additions & 1 deletion app/contrib-champs/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,10 @@ import { usePullRequests } from '@/hooks/usePullRequests'
import { PullRequestTable } from '@/components/PullRequestTable'
import { SessionsSidebar } from '@/components/SessionsSidebar'
import { sessions } from '@/lib/sessions'
import { formatUpdatedAgo } from '@/lib/snapshot'

export default function ContribChampsPage() {
const { pullRequests, loading, error } = usePullRequests()
const { pullRequests, generatedAt, loading, error } = usePullRequests()

return (
<div className="contrib-champs-layout">
Expand Down Expand Up @@ -34,6 +35,19 @@ export default function ContribChampsPage() {
Backstage and Community Plugins repositories — huge thanks to everyone who made them
happen!
</p>
{generatedAt && (
<p
style={{
fontSize: '13px',
color: 'var(--bui-fg-secondary, #666)',
marginTop: '8px',
marginBottom: 0,
}}
title={new Date(generatedAt).toLocaleString()}
>
GitHub data updated {formatUpdatedAgo(generatedAt)}
</p>
)}
</div>

{/* Loading State */}
Expand Down
6 changes: 6 additions & 0 deletions app/getting-started/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -160,6 +160,12 @@ const sidebarResources: ResourceCard[] = [
description: 'Details about the Backend System used by Backstage.',
url: 'https://backstage.io/docs/backend-system/',
isExternal: true
},
{
title: 'Using AI Tools',
description: 'AI assistants are welcome, but you must understand and test every change, and mark largely AI-generated PRs as such.',
url: 'https://github.com/backstage/backstage/blob/master/CONTRIBUTING.md#ai-use-policy-and-guidelines',
isExternal: true
}
];

Expand Down
13 changes: 13 additions & 0 deletions app/ideas/layout.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import type { Metadata } from 'next'

export const metadata: Metadata = {
title: 'Your Ideas',
}

export default function IdeasLayout({
children,
}: {
children: React.ReactNode
}) {
return <>{children}</>
}
196 changes: 196 additions & 0 deletions app/ideas/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,196 @@
import { RiExternalLinkLine } from '@remixicon/react'
import { PathChooser } from '@/components/ideas/PathChooser'
import { IdeaCanvas } from '@/components/ideas/IdeaCanvas'
import { CopyButton } from '@/components/ideas/CopyButton'
import { aiPolicyPoints, aiPolicyUrl, coachPrompts, nextSteps, topicAreas } from '@/lib/ideas'

export default function IdeasPage() {
return (
<div>
<div style={{ marginBottom: '32px' }}>
<h1 style={{ fontSize: '32px', fontWeight: 700, marginBottom: '12px', color: 'var(--bui-fg-primary, #000)' }}>
💡 Your Ideas
</h1>
<p style={{ fontSize: '16px', color: 'var(--bui-fg-secondary, #666)', lineHeight: '1.6', margin: 0 }}>
ContribFest isn&apos;t only about closing issues. Your experience running and building on Backstage is
the best source of ideas for where it should go next. This page helps you shape an idea and find the
right way to propose it. The writing is yours, and the hosts are here to help.
</p>
</div>

<Section title="1. Pick your path" intro="Answer a question or two to find the right way to propose your idea.">
<PathChooser />
</Section>

<Section
title="2. Need inspiration?"
intro="Not sure where to start? Explore an area, think about what you've run into with it, and look at what other adopters are asking for."
>
<div
className="repo-resources-grid"
style={{ display: 'grid', gridTemplateColumns: 'repeat(3, 1fr)', gap: '16px' }}
>
{topicAreas.map((topic) => (
<div key={topic.title} style={cardStyle}>
<h3 style={{ fontSize: '17px', fontWeight: 600, margin: '0 0 8px', color: 'var(--bui-fg-primary, #000)' }}>
<span aria-hidden="true">{topic.emoji}</span> {topic.title}
</h3>
<p style={{ fontSize: '14px', lineHeight: '1.5', color: 'var(--bui-fg-secondary, #666)', margin: '0 0 8px' }}>
{topic.summary}
</p>
<ul style={{ paddingLeft: '18px', margin: '0 0 12px', fontSize: '14px', lineHeight: '1.5', color: 'var(--bui-fg-primary, #000)', flex: 1 }}>
{topic.questions.map((question) => (
<li key={question}>{question}</li>
))}
</ul>
<div style={{ display: 'flex', flexWrap: 'wrap', gap: '12px' }}>
<ExternalLink href={topic.docsUrl}>Docs</ExternalLink>
<ExternalLink href={topic.codeUrl}>Code</ExternalLink>
<ExternalLink href={topic.issuesUrl}>Top requests</ExternalLink>
</div>
</div>
))}
</div>
</Section>

<Section
title="3. Think it through"
intro="Jot down answers in your own words. These questions follow the BEP template, so your notes carry over to whichever path you pick. Nothing is sent anywhere."
>
<IdeaCanvas />
</Section>

<Section
title="4. Use AI as a coach, not an author"
intro="AI assistants are great for exploring the codebase and challenging your thinking. Copy a prompt into your own assistant and fill in the brackets. Each one asks the AI to explain, search, or question, not to write your proposal."
>
<div
style={{
...cardStyle,
marginBottom: '16px',
background: 'var(--contribfest-progress-bg, #dcfce7)',
}}
>
<h3 style={{ fontSize: '17px', fontWeight: 600, margin: '0 0 8px', color: 'var(--bui-fg-primary, #000)' }}>
Backstage AI Use Policy
</h3>
<ul style={{ paddingLeft: '18px', margin: '0 0 12px', fontSize: '14px', lineHeight: '1.6', color: 'var(--bui-fg-primary, #000)' }}>
{aiPolicyPoints.map((point) => (
<li key={point}>{point}</li>
))}
</ul>
<ExternalLink href={aiPolicyUrl}>Read the full policy</ExternalLink>
</div>

<div
className="session-resources-grid"
style={{ display: 'grid', gridTemplateColumns: 'repeat(2, 1fr)', gap: '16px' }}
>
{coachPrompts.map((coachPrompt) => (
<div key={coachPrompt.title} style={cardStyle}>
<h3 style={{ fontSize: '17px', fontWeight: 600, margin: '0 0 4px', color: 'var(--bui-fg-primary, #000)' }}>
{coachPrompt.title}
</h3>
<p style={{ fontSize: '14px', color: 'var(--bui-fg-secondary, #666)', margin: '0 0 12px' }}>
{coachPrompt.purpose}
</p>
<blockquote
style={{
margin: '0 0 12px',
padding: '12px',
borderLeft: '3px solid var(--bui-bg-solid, #1f5493)',
background: 'var(--bui-bg-app, #f8f8f8)',
fontSize: '14px',
lineHeight: '1.5',
color: 'var(--bui-fg-primary, #000)',
flex: 1,
}}
>
{coachPrompt.prompt}
</blockquote>
<div>
<CopyButton text={coachPrompt.prompt} label="Copy prompt" />
</div>
</div>
))}
</div>
</Section>

<Section title="5. From idea to reality" intro="Big ideas land through conversation and steady, small steps.">
<ol style={{ listStyle: 'none', padding: 0, margin: 0, display: 'flex', flexDirection: 'column', gap: '12px' }}>
{nextSteps.map((step, index) => (
<li key={step.title} style={{ ...cardStyle, flexDirection: 'row', gap: '16px', alignItems: 'flex-start' }}>
<span
aria-hidden="true"
style={{
flexShrink: 0,
width: '32px',
height: '32px',
borderRadius: '50%',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
fontWeight: 700,
background: 'var(--bui-bg-solid, #1f5493)',
color: 'var(--bui-fg-solid, #fff)',
}}
>
{index + 1}
</span>
<div>
<h3 style={{ fontSize: '16px', fontWeight: 600, margin: '0 0 4px', color: 'var(--bui-fg-primary, #000)' }}>
{step.title}
</h3>
<p style={{ fontSize: '14px', lineHeight: '1.5', color: 'var(--bui-fg-secondary, #666)', margin: 0 }}>
{step.description}
</p>
{step.link && (
<div style={{ marginTop: '8px' }}>
<ExternalLink href={step.link.url}>{step.link.label}</ExternalLink>
</div>
)}
</div>
</li>
))}
</ol>
</Section>
</div>
)
}

const cardStyle: React.CSSProperties = {
border: '1px solid var(--bui-border-1, #d5d5d5)',
borderRadius: '8px',
padding: '20px',
background: 'var(--bui-bg-popover, #fff)',
display: 'flex',
flexDirection: 'column',
}

function Section({ title, intro, children }: { title: string; intro: string; children: React.ReactNode }) {
return (
<section style={{ marginBottom: '48px' }}>
<h2 style={{ fontSize: '24px', fontWeight: 700, margin: '0 0 8px', color: 'var(--bui-fg-primary, #000)' }}>
{title}
</h2>
<p style={{ fontSize: '16px', lineHeight: '1.6', color: 'var(--bui-fg-secondary, #666)', margin: '0 0 20px' }}>
{intro}
</p>
{children}
</section>
)
}

function ExternalLink({ href, children }: { href: string; children: React.ReactNode }) {
return (
<a
href={href}
target="_blank"
rel="noopener noreferrer"
style={{ display: 'inline-flex', alignItems: 'center', gap: '4px', fontSize: '14px', fontWeight: 600, color: 'var(--bui-bg-solid, #1f5493)' }}
>
{children}
<RiExternalLinkLine size={14} aria-hidden="true" />
</a>
)
}
Loading
Loading