Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
22a87d2
feat: add docs for release changes
switzerb May 8, 2026
1170624
feat: implementation of release flow
switzerb May 8, 2026
4de84ab
feat: add dry-run option for testing
switzerb May 8, 2026
364067b
Apply suggestion from @colehart
switzerb May 21, 2026
f3db1f0
fix: clean up yaml formatting
switzerb May 21, 2026
0a3b243
Merge remote-tracking branch 'origin/feat/beta-experimental-releases'…
switzerb May 21, 2026
1aa9ab3
Apply suggestion from @colehart
switzerb May 21, 2026
64d173a
Apply suggestion from @colehart
switzerb May 21, 2026
bcfceda
Apply suggestion from @colehart
switzerb May 21, 2026
6d717ae
fix: re-work age tracker to use github api workflow runs
switzerb May 21, 2026
6f417eb
Apply suggestion from @colehart
switzerb May 21, 2026
abf59d8
Merge remote-tracking branch 'origin/feat/beta-experimental-releases'…
switzerb May 21, 2026
a34af85
fix: make age tracker easier to read the logic and tidy docs
switzerb May 21, 2026
7d3ebd5
fix: move the command to the package.json file
switzerb May 21, 2026
fde7fae
fix: simplify verification of branch step
switzerb May 21, 2026
32a4965
fix: simplify experimental workflow
switzerb May 21, 2026
d5a0142
Apply suggestion from @colehart
switzerb May 21, 2026
1dd74ad
fix: needs a temporary push command to register
switzerb May 21, 2026
6116212
Merge remote-tracking branch 'origin/feat/beta-experimental-releases'…
switzerb May 21, 2026
6cdf7f0
fix: edit message for unpublished experiment
switzerb May 21, 2026
ca36b5f
fix: minor tweaks for clarity
switzerb May 21, 2026
aa0396a
fix: minor tweaks for clarity
switzerb May 21, 2026
f8e2aa5
feat: set dry run to true in case this gets run accidentally
switzerb May 21, 2026
dbab874
feat: update docs
switzerb May 21, 2026
555077e
fix: remove the time gap in the age tracker
switzerb May 21, 2026
7bd2231
fix: remove unnecessary duplicate declaration
switzerb May 21, 2026
840d5f4
fix: moved it too far up!
switzerb May 21, 2026
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
106 changes: 106 additions & 0 deletions .github/PULL_REQUEST_TEMPLATE/experimental.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
<!--
🧪 EXPERIMENTAL PR TEMPLATE

This PR tracks an experimental branch for rapid API exploration and feedback.

⚠️ IMPORTANT: This PR will NEVER merge to main.
Successful experiments are reimplemented with full quality bar on a fresh PR.

Time-box: 1 month maximum
See docs/EXPERIMENTAL_RELEASE.md for full workflow details.
-->

## 🧪 Experiment Overview

**Feature Name:** `<feature-name>` (from `experimental/<feature-name>` branch)

**Goal:**
<!-- What API or pattern are you exploring? What problem does this solve? -->

**Success Criteria:**
<!-- How will you know if this experiment succeeded? What feedback are you looking for? -->

**Affected Packages:**
<!-- List 1-3 packages this experiment touches (keep it focused) -->
- [ ] `@accelint/<package-name>`

---

## 📋 Experiment Plan

**Week 1:**
<!-- Implement minimal API sketch, publish first snapshot -->

**Week 2:**
<!-- Gather feedback, iterate on design -->

**Week 3:**
<!-- Decision checkpoint: trending toward success or abandon? -->

**Week 4:**
<!-- Final decision: promote (reimplement) or abandon (document) -->

---

## 🔍 Findings & Feedback

<!-- Update this section as you gather feedback -->

### What's Working
<!-- API patterns, approaches, or designs that are promising -->

### What's Not Working
<!-- Problems discovered, limitations, or concerns -->

### Open Questions
<!-- Unresolved questions that need input -->

---

## 📦 Installation

Once published, stakeholders can install with:

```bash
npm install @accelint/<package>@<feature-name>
```

Publish experimental snapshots via **Actions → Publish Experimental Snapshot → Run workflow**.

---

## ⚠️ Experimental Status

- **Quality Bar:** Minimal (build + types + lint only)
- **Tests:** Not required for experimental
- **Docs:** Not required for experimental
- **Time-Box:** 1 month maximum
- **Never Merges:** This PR will not merge to main

**Age Tracking:** Automated workflow runs daily with escalating warnings at 2, 3, and 4 weeks.

---

## 🎯 Final Decision

<!-- Complete before closing this PR -->

### Decision: [Promote / Abandon]

**If Promoting:**
- [ ] Close this experimental PR
- [ ] Delete experimental branch
- [ ] Open fresh PR to main with full implementation (tests + docs + review)
- [ ] Reference this PR in new PR for context

**If Abandoning:**
- [ ] Document lessons learned below
- [ ] Close this experimental PR
- [ ] Delete experimental branch

**Lessons Learned:**
<!-- What did we learn from this experiment? What should we do differently? -->

---

📚 **Documentation:** [Experimental Release Workflow](../docs/EXPERIMENTAL_RELEASE.md)
161 changes: 161 additions & 0 deletions .github/workflows/experimental-age-tracker.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,161 @@
name: Experimental Age Tracker
on:
schedule:
# Run daily at 9 AM UTC
- cron: '0 9 * * *'
workflow_dispatch:
jobs:
track-age:
name: Track Experimental Branch Age
runs-on: ubuntu-latest
permissions:
pull-requests: write # to create/update PR comments
steps:
- uses: actions/checkout@v5

- name: Track experimental PR ages
uses: actions/github-script@v7
with:
script: |
// Find all open PRs from experimental/* branches
const { data: prs } = await github.rest.pulls.list({
owner: context.repo.owner,
repo: context.repo.repo,
state: 'open',
per_page: 100
});

const experimentalPRs = prs.filter(pr =>
pr.head.ref.startsWith('experimental/')
);

console.log(`Found ${experimentalPRs.length} experimental PRs`);

if (experimentalPRs.length === 0) {
console.log('No experimental PRs to track');
return;
}

// Process each experimental PR
for (const pr of experimentalPRs) {
console.log(`\nProcessing PR #${pr.number}: ${pr.title}`);

try {
// Query workflow runs to find first successful publish
const { data: runs } = await github.rest.actions.listWorkflowRuns({
owner: context.repo.owner,
repo: context.repo.repo,
workflow_id: 'publish-experimental.yml',
branch: pr.head.ref,
status: 'success',
per_page: 100
});

// Calculate age from first successful publish
let ageInDays, publishDate, dateStr;

// Calculate deadline and remaining time
const currentDate = new Date();

if (runs.workflow_runs.length === 0) {
console.log(` ⚠️ No successful publish runs found.`);
ageInDays = 'N/A';
dateStr = 'Not published';
} else {
const firstRun = runs.workflow_runs
.sort((a, b) => new Date(a.created_at) - new Date(b.created_at))[0];

publishDate = new Date(firstRun.created_at);
dateStr = publishDate.toISOString().split('T')[0];
ageInDays = Math.floor((currentDate - publishDate) / (1000 * 60 * 60 * 24));

console.log(` 📅 First published: ${dateStr}`);
console.log(` ⏱️ Age: ${ageInDays} days`);
}

const deadline = publishDate ? new Date(publishDate.getTime() + 28 * 24 * 60 * 60 * 1000) : null;
const daysRemaining = deadline ? Math.floor((deadline - currentDate) / (1000 * 60 * 60 * 24)) : 0;

// Determine status based on age
let emoji, status, nextSteps;

if (ageInDays === 'N/A') {
emoji = '⚠️';
status = 'Unpublished';
nextSteps = `Once you publish, you will have a month for experimentation. Do not use your experiment in a production environment.`;
} else if (ageInDays > 27) {
emoji = '🚨';
status = 'Overdue! - time to make a decision';
nextSteps = `This experiment has exceeded the 1-month time-box.\n\n**Required actions:**\n1. **Promote:** Close this PR, reimplement with full quality bar (tests, docs, review), open fresh PR to main\n2. **Abandon:** Close this PR, delete experimental branch, document why`;
} else if (ageInDays >= 20) {
emoji = '⏰';
status = 'Approaching deadline';
nextSteps = `Deadline approaching in **${daysRemaining} days**.\n\n- Begin decision process: promote or abandon?\n- If promoting, plan full implementation with tests and docs\n- If abandoning, document lessons learned`;
} else if (ageInDays >= 13) {
emoji = '⏰';
status = 'Active';
nextSteps = `You are two weeks in! You have **${daysRemaining} days remaining** to evaluate this experiment.`;
} else {
emoji = 'ℹ️';
status = 'Active';
nextSteps = `You have **${daysRemaining} days remaining** to evaluate this experiment.\n\n- Test the experimental build\n- Gather feedback from users\n- Document findings`;
}

console.log(` ${emoji} Status: ${status}`);

// Fetch comments for status tracking
const { data: comments } = await github.rest.issues.listComments({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: pr.number
});

// Generate status comment
const statusCommentBody = [
'## 📊 Experiment Status',
'',
`**Status:** ${emoji} ${status}`,
`**Published:** ${dateStr}`,
`**Age In Days:** ${ageInDays}`,
`**Deadline:** ${deadline ? deadline.toISOString().split('T')[0] : 'Countdown clock not started.'}`,
'',
'### Next Steps',
nextSteps,
'',
'---',
`*Last updated: ${currentDate.toUTCString()}*`
].join('\n');

// Find existing status comment
const statusComment = comments.find(comment =>
comment.user.type === 'Bot' &&
comment.body.includes('📊 Experiment Status')
);

if (statusComment) {
// Update existing status comment
await github.rest.issues.updateComment({
owner: context.repo.owner,
repo: context.repo.repo,
comment_id: statusComment.id,
body: statusCommentBody
});
console.log(` ✓ Updated status comment`);
} else {
// Create new status comment
await github.rest.issues.createComment({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: pr.number,
body: statusCommentBody
});
console.log(` ✓ Created status comment`);
}

} catch (error) {
console.error(` ❌ Error processing PR #${pr.number}: ${error.message}`);
// Continue to next PR
}
}

console.log('\n✓ Age tracking complete');
Loading
Loading