mirror of
https://github.com/paperclipai/paperclip.git
synced 2026-10-02 02:07:25 +08:00
## Thinking Path > - Paperclip helps people manage AI agents for work. > - Maintainers use Storybook to review the board UI. > - Reviews need public previews of selected repository branches. > - Each branch needs its own URL so previews do not replace each other. > - This pull request adds manual, CODEOWNER-controlled publishing to S3 and CloudFront. > - The action returns stable branch links and permanent build links in its summary and a Markdown artifact. ## Linked Issues or Issue Description **What existing behavior does this improve?** The existing Storybook build and manual visual-review workflow. **Current behavior** The repository has no manual branch-preview publisher. A single GitHub Pages site cannot support independent publishers without combining their output. **Proposed behavior** A CODEOWNER selects a source branch and approves publication. Each branch has a stable CloudFront URL. A completed build becomes the branch target only after its upload succeeds. The action attaches `storybook-deployment.md` with the preview links and source commit. **Reason and benefit** Maintainers can share multiple branch previews at the same time. Branch builds have no repository token permissions or AWS credentials. Dependency caching and install hooks are disabled. The publisher cannot write runner dashboard files or delete objects. **Breaking changes** None. Normal visual checks keep their existing behavior. This does not change application code or GitHub Pages settings. **Additional context** Searched public issues and PRs for Storybook deployment work. No duplicate deployment proposal was found. This is maintainer infrastructure, not a roadmap-level core feature. ## What Changed - Add `Storybook Deploy` with a source-branch input and a manual entry through `Storybook Visual`. - Check the original actor and rerunner against default-branch CODEOWNERS. Require a protected deployment environment with CODEOWNER reviewers. - Separate public-source builds with no repository permissions from an OIDC publisher restricted to the Storybook S3 prefix. - Publish distinct branch URLs and retain build URLs. Preserve Storybook deep links across the branch redirect. - Add the run summary, a downloadable Markdown deployment report, focused tests, and operator setup docs and IAM policies. ## Verification - `node --test scripts/__tests__/storybook-deploy.test.mjs`: 19 tests pass. - `actionlint .github/workflows/storybook-deploy.yml .github/workflows/storybook-visual.yml`: passes. - [Feature branch live publication and deployment-only rerun](https://github.com/paperclipai/paperclip/actions/runs/34533202273): passed. - [Master branch live publication](https://github.com/paperclipai/paperclip/actions/runs/34533204743): passed. - Both public branch URLs render a component story without browser errors. A deployment-only rerun updates only the selected branch entry and preserves the previous build URL. - AWS policy simulation allows Storybook uploads and denies dashboard writes and object deletion. - Full local typechecking passes. Full local tests, build, and current-head PR checks are running. - [Revised build and Markdown artifact validation](https://github.com/paperclipai/paperclip/actions/runs/34605623088): passed. Downloaded the report and verified its branch URL, build URL, and source commit. - The public verifier also checks that the stable branch URL points to this build and rejects stale targets. ## Risks - Storybook previews are public. Maintainers must publish only public UI fixtures. - Retained builds accumulate until an operator prunes them. - Environment reviewers must stay synchronized with CODEOWNERS. The workflow fails closed if its environment loses required protection. - The existing CloudFront distribution is shared with runner reports. Separate S3 prefixes and a dedicated role prevent the publisher from overwriting those reports. ## Model Used OpenAI GPT-6 via Codex, with reasoning, shell tools, and browser 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 - [ ] All Paperclip CI gates are green - [ ] 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>
51 lines
2.4 KiB
JavaScript
51 lines
2.4 KiB
JavaScript
// Run before building, and again inside the protected deployment job on reruns.
|
|
module.exports = async function authorizeStorybookDeploy({ github, context }) {
|
|
const fail = (message) => { throw new Error(message); };
|
|
if (context.repo.owner !== "paperclipai" || context.repo.repo !== "paperclip") {
|
|
fail("Storybook publishing is restricted to paperclipai/paperclip.");
|
|
}
|
|
if (context.eventName !== "workflow_dispatch" || !context.ref.startsWith("refs/heads/")) {
|
|
fail("Storybook publishing requires a manual run from a repository branch.");
|
|
}
|
|
|
|
// The selected branch must never be able to add itself to the allowlist.
|
|
const { data: repository } = await github.rest.repos.get(context.repo);
|
|
const { data: file } = await github.rest.repos.getContent({
|
|
...context.repo,
|
|
path: ".github/CODEOWNERS",
|
|
ref: repository.default_branch,
|
|
});
|
|
if (file.encoding !== "base64" || typeof file.content !== "string") {
|
|
fail("Cannot read the default branch CODEOWNERS file.");
|
|
}
|
|
const owners = new Set();
|
|
for (const line of Buffer.from(file.content, "base64").toString("utf8").split(/\r?\n/)) {
|
|
const fields = line.split("#", 1)[0].trim().split(/\s+/);
|
|
for (const owner of fields.slice(1)) {
|
|
// Individual GitHub accounts only. Teams/email entries do not grant access.
|
|
if (/^@[a-z\d](?:[a-z\d-]*[a-z\d])?$/i.test(owner)) {
|
|
owners.add(owner.slice(1).toLowerCase());
|
|
}
|
|
}
|
|
}
|
|
if (owners.size === 0) fail("CODEOWNERS has no individual GitHub accounts.");
|
|
for (const actor of [context.actor, process.env.GITHUB_TRIGGERING_ACTOR]) {
|
|
if (!actor || !owners.has(actor.toLowerCase())) {
|
|
fail(`Only default-branch CODEOWNERS may publish Storybook (${actor || "missing actor"}).`);
|
|
}
|
|
}
|
|
|
|
// A branch can edit its workflow. Require a GitHub-enforced CODEOWNER review
|
|
// as well, so editing this check cannot grant an outsider deployment access.
|
|
const { data: environment } = await github.rest.repos.getEnvironment({
|
|
...context.repo,
|
|
environment_name: "storybook-deploy",
|
|
});
|
|
const reviewers = environment.protection_rules
|
|
?.find((rule) => rule.type === "required_reviewers")?.reviewers;
|
|
if (environment.can_admins_bypass !== false || !reviewers?.length ||
|
|
reviewers.some(({ type, reviewer }) => type !== "User" || !owners.has(reviewer.login.toLowerCase()))) {
|
|
fail("storybook-deploy must require CODEOWNER reviewers and disable administrator bypass.");
|
|
}
|
|
};
|