mirror of
https://github.com/paperclipai/paperclip.git
synced 2026-10-06 21:05:21 +02:00
## Thinking Path > - Paperclip helps people manage AI agents for work. > - Maintainers use Storybook previews to review the board UI. > - Branch previews need stable bookmarks that people can read. > - The current publisher only provides a hashed branch path. > - This pull request adds a readable branch bookmark after each successful upload. > - Existing branch and build links keep working. ## Linked Issues or Issue Description Refs #13226. **What existing behavior does this improve?** Manual Storybook publication for repository branches. **Current behavior** The stable branch path contains a hash. The expected `/storybook/branches/master/` URL does not exist. **Proposed behavior** Each publication updates a readable bookmark. The action summary and Markdown artifact link it. Master uses `/storybook/branches/master/`. Other names use a safe path segment that preserves case and escapes special characters. **Reason and benefit** Maintainers can save and share a readable URL that opens the latest published branch build. **Breaking changes** None. Existing hashed branch entries still update. Existing build URLs remain valid. **Additional context** This follows the publisher in #13226. A duplicate search found no related bookmark change. It does not overlap planned core work in ROADMAP.md. ## What Changed - Generate readable branch bookmarks without collisions with existing build directories. - Upload the bookmark only after the full build and compatibility entry uploads succeed. - Link the bookmark in the existing summary and Markdown artifact. - Document branch-name escaping and test path isolation, stable links, and upload order. ## Verification - `node --test scripts/__tests__/storybook-deploy.test.mjs`: 20 tests pass. - `actionlint .github/workflows/storybook-deploy.yml .github/workflows/storybook-visual.yml`: passes. - `git diff --check`: passes. - [Master bookmark publication](https://github.com/paperclipai/paperclip/actions/runs/34613344758): passed. Opened `/storybook/branches/master/` in the browser and confirmed a story renders. Downloaded the Markdown report and verified its bookmark link. - [Feature branch bookmark publication](https://github.com/paperclipai/paperclip/actions/runs/34613449034): passed. Its separate bookmark uses `codex~2Fstorybook-bookmarks`. - Greptile: 5/5 on `dccaf10413ecf447cb34e622b6b3c505791abb51`, with no unresolved review threads. All current-head Paperclip CI gates pass, including typecheck, tests, build, browser suites, and the canary dry run. - Full local repository checks were not repeated for this focused publisher change. The preceding run passed typecheck but encountered unrelated native-session test failures. ## Risks - Special characters in branch names use `~HH` byte escapes. For example, `feature/foo` becomes `feature~2Ffoo`. - Names that could overlap an existing hashed build directory escape the final hyphen. Very long names retain a hash suffix. - The two branch entries update separately. If the final upload fails, the workflow fails and a rerun can repair the bookmark. ## Model Used OpenAI GPT-6 via Codex, with reasoning, shell tools, and live deployment verification. The exact runtime model ID and context-window size are not exposed in this session. ## Checklist - [x] I have included a thinking path that traces from project context to this change - [x] I have specified the model used (with version and capability details) - [x] I have checked ROADMAP.md and confirmed this PR does not duplicate planned core work - [x] I have searched GitHub for duplicate or related PRs and linked them above - [x] I have either (a) linked existing issues with `Fixes: #` / `Closes #` / `Refs #` OR (b) described the issue in-PR following the relevant issue template - [x] I have not referenced internal/instance-local Paperclip issues or links (only public GitHub `#NNN` / `github.com/paperclipai/paperclip` URLs) - [x] My branch name describes the change (e.g. `docs/...`, `fix/...`) and contains no internal Paperclip ticket id or instance-derived details - [x] I have run tests locally and they pass - [x] I have added or updated tests where applicable - [x] I have updated relevant documentation to reflect my changes - [x] I have considered and documented any risks above - [x] All Paperclip CI gates are green - [x] Greptile is 5/5 with no open P2s, recommendations, or follow-ups - [x] I will address all Greptile and reviewer comments before requesting merge Co-authored-by: Paperclip <noreply@paperclip.ing>
48 lines
2.8 KiB
JavaScript
48 lines
2.8 KiB
JavaScript
const { createHash } = require('node:crypto');
|
|
|
|
function storybookDestination({ branch, sha, runId, runAttempt, bucket, baseUrl }) {
|
|
if (typeof branch !== 'string' || !branch || /[\x00-\x20\x7f]/.test(branch)) {
|
|
throw new Error('A non-empty repository branch name is required.');
|
|
}
|
|
if (!/^[a-f0-9]{40}$/.test(sha)) throw new Error('A full source commit SHA is required.');
|
|
if (![runId, runAttempt].every((value) => /^[1-9]\d*$/.test(String(value)))) {
|
|
throw new Error('A valid workflow run and attempt are required.');
|
|
}
|
|
if (!/^[a-z0-9][a-z0-9.-]{1,61}[a-z0-9]$/.test(bucket)) throw new Error('Invalid Storybook S3 bucket.');
|
|
const base = new URL(baseUrl);
|
|
if (base.protocol !== 'https:' || base.username || base.password || base.search || base.hash || base.pathname !== '/') {
|
|
throw new Error('Storybook base URL must be a credential-free HTTPS origin.');
|
|
}
|
|
const label = branch.toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-|-$/g, '').slice(0,60) || 'branch';
|
|
const digest = createHash('sha256').update(branch).digest('hex').slice(0,16);
|
|
const branchKey = `${label}-${digest}`;
|
|
const prefix = `storybook/branches/${branchKey}`;
|
|
const buildPrefix = `${prefix}/builds/${runId}-${runAttempt}`;
|
|
// Use one reversible path segment: slashes and special characters become
|
|
// ~HH UTF-8 bytes, so feature/foo and feature-foo never share a bookmark.
|
|
let bookmarkKey = [...Buffer.from(branch)].map((byte) =>
|
|
/[A-Za-z0-9_-]/.test(String.fromCharCode(byte))
|
|
? String.fromCharCode(byte) : `~${byte.toString(16).toUpperCase().padStart(2, '0')}`).join('');
|
|
// Reserve the existing hashed directories, including all immutable builds.
|
|
bookmarkKey = bookmarkKey.replace(/-([a-f0-9]{16})$/, '~2D$1');
|
|
// Keep arbitrarily long ref names within S3's object-key limit. ~long cannot
|
|
// occur in the reversible encoding, whose escapes contain only hex digits.
|
|
if (bookmarkKey.length > 900) bookmarkKey = `${bookmarkKey.slice(0, 800)}~long-${digest}`;
|
|
const bookmarkPrefix = `storybook/branches/${bookmarkKey}`;
|
|
return {
|
|
branch, sha, bucket, branchKey, prefix, buildPrefix, bookmarkPrefix,
|
|
url: `${base.origin}/${bookmarkPrefix}/`,
|
|
legacyUrl: `${base.origin}/${prefix}/index.html`,
|
|
buildUrl: `${base.origin}/${buildPrefix}/index.html`,
|
|
};
|
|
}
|
|
|
|
function branchIndex(buildUrl) {
|
|
// The target is generated from a validated origin and ASCII path segments.
|
|
const target = JSON.stringify(buildUrl).replace(/</g, '\\u003c');
|
|
return `<!doctype html><html lang="en"><meta charset="utf-8"><title>Storybook preview</title>
|
|
<script>const target = new URL(${target}); target.search = location.search; target.hash = location.hash; location.replace(target.href);</script>
|
|
<noscript><a href="${buildUrl}">Open this branch's Storybook</a></noscript></html>\n`;
|
|
}
|
|
module.exports = { storybookDestination, branchIndex };
|