@theholocron/cli 3.31.0 → 3.32.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/capabilities/index.d.mts +6 -0
- package/dist/cli.mjs +171 -5
- package/dist/cli.mjs.map +1 -1
- package/package.json +3 -3
|
@@ -366,6 +366,12 @@ interface Deployment extends ProviderIdentity {
|
|
|
366
366
|
target?: DeploymentTrigger;
|
|
367
367
|
}): Promise<DeploymentRecord>;
|
|
368
368
|
getDeployment(deploymentId: string): Promise<DeploymentRecord>;
|
|
369
|
+
/**
|
|
370
|
+
* Add a custom domain (or wildcard) to the project. Idempotent — no-op
|
|
371
|
+
* when the domain is already present. Optional: providers without a custom
|
|
372
|
+
* domain API omit this (e.g. Vercel manages domains separately).
|
|
373
|
+
*/
|
|
374
|
+
ensureCustomDomain?(projectId: string, hostname: string): Promise<void>;
|
|
369
375
|
}
|
|
370
376
|
interface StorageBranch {
|
|
371
377
|
id: string;
|
package/dist/cli.mjs
CHANGED
|
@@ -2830,9 +2830,106 @@ function generateThinCallerContent(name, withOverrides, additionalPaths) {
|
|
|
2830
2830
|
return injected;
|
|
2831
2831
|
}
|
|
2832
2832
|
/**
|
|
2833
|
+
* Extract the Cloudflare Pages preview config from a deploy workflow's `with:` object.
|
|
2834
|
+
*
|
|
2835
|
+
* Accepts three forms:
|
|
2836
|
+
* - `preview: true` — derive both project and domain from org context
|
|
2837
|
+
* - `preview: { project: "..." }` — explicit project; domain derived from context if omitted
|
|
2838
|
+
* - `preview: { project: "...", domain: "..." }` — fully explicit
|
|
2839
|
+
*
|
|
2840
|
+
* Returns null when `preview:` is absent, false, or can't be resolved.
|
|
2841
|
+
*/
|
|
2842
|
+
function extractPreviewConfig(raw, ctx = {}) {
|
|
2843
|
+
const preview = raw["preview"];
|
|
2844
|
+
if (!preview) return null;
|
|
2845
|
+
if (preview === true) {
|
|
2846
|
+
const project = ctx.org ? `${ctx.org}-preview` : null;
|
|
2847
|
+
const domain = ctx.docsDomain ? `preview.${ctx.docsDomain}` : void 0;
|
|
2848
|
+
if (!project) return null;
|
|
2849
|
+
return {
|
|
2850
|
+
project,
|
|
2851
|
+
...domain ? { domain } : {}
|
|
2852
|
+
};
|
|
2853
|
+
}
|
|
2854
|
+
if (typeof preview !== "object") return null;
|
|
2855
|
+
const p = preview;
|
|
2856
|
+
const project = typeof p["project"] === "string" && p["project"] ? p["project"] : ctx.org ? `${ctx.org}-preview` : null;
|
|
2857
|
+
if (!project) return null;
|
|
2858
|
+
const domain = typeof p["domain"] === "string" && p["domain"] ? p["domain"] : ctx.docsDomain ? `preview.${ctx.docsDomain}` : void 0;
|
|
2859
|
+
return {
|
|
2860
|
+
project,
|
|
2861
|
+
...domain ? { domain } : {}
|
|
2862
|
+
};
|
|
2863
|
+
}
|
|
2864
|
+
/**
|
|
2865
|
+
* Generate the full thin-caller YAML for a `deploy.yml` that handles both
|
|
2866
|
+
* production (push to main → GitHub Pages) and preview (pull_request →
|
|
2867
|
+
* Cloudflare Pages) in a single file.
|
|
2868
|
+
*
|
|
2869
|
+
* Both jobs receive the same docs/storybook `with:` inputs. If the per-repo
|
|
2870
|
+
* config supplies `cloudflare-project` it is forwarded; otherwise the reusable
|
|
2871
|
+
* falls back to the `CLOUDFLARE_PAGES_PROJECT` org variable — set that once and
|
|
2872
|
+
* all repos with a `deploy` workflow get previews without per-repo config.
|
|
2873
|
+
*/
|
|
2874
|
+
function generateCombinedDeployContent(deployWith, paths, preview) {
|
|
2875
|
+
const yamlScalar = (v) => {
|
|
2876
|
+
if (v === true) return "true";
|
|
2877
|
+
if (v === false) return "false";
|
|
2878
|
+
const s = String(v);
|
|
2879
|
+
return s.startsWith("[") || s.startsWith("{") ? `'${s}'` : s;
|
|
2880
|
+
};
|
|
2881
|
+
const withLines = (entries) => Object.entries(entries).map(([k, v]) => ` ${k}: ${yamlScalar(v)}`).join("\n");
|
|
2882
|
+
const pathsBlock = paths.length > 0 ? ` paths:\n${paths.map((p) => ` - ${p}\n`).join("")}` : "";
|
|
2883
|
+
const previewWith = {
|
|
2884
|
+
...deployWith,
|
|
2885
|
+
"cloudflare-project": preview.project
|
|
2886
|
+
};
|
|
2887
|
+
const deployWithBlock = Object.keys(deployWith).length > 0 ? ` with:\n${withLines(deployWith)}\n` : "";
|
|
2888
|
+
const previewWithBlock = ` with:\n${withLines(previewWith)}\n`;
|
|
2889
|
+
return [
|
|
2890
|
+
`name: Deploy`,
|
|
2891
|
+
``,
|
|
2892
|
+
`on: # yamllint disable-line rule:truthy`,
|
|
2893
|
+
` push:`,
|
|
2894
|
+
` branches: [main]`,
|
|
2895
|
+
...pathsBlock ? [`${pathsBlock}`] : [],
|
|
2896
|
+
` pull_request:`,
|
|
2897
|
+
` branches: [main]`,
|
|
2898
|
+
...pathsBlock ? [`${pathsBlock}`] : [],
|
|
2899
|
+
` workflow_dispatch:`,
|
|
2900
|
+
``,
|
|
2901
|
+
`concurrency:`,
|
|
2902
|
+
` group: $\{{ github.event_name == 'pull_request' && format('deploy-preview-{0}', github.event.pull_request.number) || 'pages' }}`,
|
|
2903
|
+
` cancel-in-progress: $\{{ github.event_name == 'pull_request' }}`,
|
|
2904
|
+
``,
|
|
2905
|
+
`permissions:`,
|
|
2906
|
+
` contents: read`,
|
|
2907
|
+
` pages: write`,
|
|
2908
|
+
` id-token: write`,
|
|
2909
|
+
` pull-requests: write`,
|
|
2910
|
+
``,
|
|
2911
|
+
`jobs:`,
|
|
2912
|
+
` deploy:`,
|
|
2913
|
+
` name: Deploy`,
|
|
2914
|
+
` if: \${{ github.event_name != 'pull_request' }}`,
|
|
2915
|
+
` uses: theholocron/.github/.github/workflows/deploy.yml@main`,
|
|
2916
|
+
...deployWithBlock ? [deployWithBlock.trimEnd()] : [],
|
|
2917
|
+
` secrets: inherit`,
|
|
2918
|
+
``,
|
|
2919
|
+
` preview:`,
|
|
2920
|
+
` name: Deploy Preview`,
|
|
2921
|
+
` if: \${{ github.event_name == 'pull_request' }}`,
|
|
2922
|
+
` uses: theholocron/.github/.github/workflows/deploy-preview.yml@main`,
|
|
2923
|
+
previewWithBlock.trimEnd(),
|
|
2924
|
+
` secrets: inherit`,
|
|
2925
|
+
``
|
|
2926
|
+
].join("\n");
|
|
2927
|
+
}
|
|
2928
|
+
/**
|
|
2833
2929
|
* Expand structured with-values to flat GitHub Actions inputs before
|
|
2834
2930
|
* generating the thin caller. Handles:
|
|
2835
2931
|
* - deploy shorthand: docs/storybook → type + storybook-projects
|
|
2932
|
+
* - preview: stripped (handled separately via extractPreviewConfig)
|
|
2836
2933
|
* - run-chromatic object → run-chromatic: true + chromatic-projects
|
|
2837
2934
|
* - plain arrays → JSON-stringified for YAML scalar quoting
|
|
2838
2935
|
*
|
|
@@ -2840,6 +2937,7 @@ function generateThinCallerContent(name, withOverrides, additionalPaths) {
|
|
|
2840
2937
|
*/
|
|
2841
2938
|
function normalizeWorkflowWith(raw) {
|
|
2842
2939
|
const result = { ...raw };
|
|
2940
|
+
delete result["preview"];
|
|
2843
2941
|
const hasDocs = raw["docs"] === true || raw["docs"] !== null && typeof raw["docs"] === "object";
|
|
2844
2942
|
const storybookProjects = raw["storybook"];
|
|
2845
2943
|
if (hasDocs) {
|
|
@@ -3407,6 +3505,20 @@ async function runSetup(input) {
|
|
|
3407
3505
|
print(formatStep(steps[steps.length - 1]));
|
|
3408
3506
|
continue;
|
|
3409
3507
|
}
|
|
3508
|
+
if (name === "deploy" && rawWith) {
|
|
3509
|
+
const previewCfg = extractPreviewConfig(rawWith, {
|
|
3510
|
+
org: config.org,
|
|
3511
|
+
docsDomain: config.docs?.domain
|
|
3512
|
+
});
|
|
3513
|
+
if (previewCfg) {
|
|
3514
|
+
const paths = additionalPaths;
|
|
3515
|
+
steps.push(await runStep("source", "write workflow deploy (with preview)", dryRun, async () => {
|
|
3516
|
+
await source.writeWorkflowFile("deploy.yml", workflowHeader() + generateCombinedDeployContent(withOverrides, paths, previewCfg));
|
|
3517
|
+
}));
|
|
3518
|
+
print(formatStep(steps[steps.length - 1]));
|
|
3519
|
+
continue;
|
|
3520
|
+
}
|
|
3521
|
+
}
|
|
3410
3522
|
steps.push(await runStep("source", `write workflow ${name}`, dryRun, async () => {
|
|
3411
3523
|
await source.writeWorkflowFile(`${name}.yml`, workflowHeader() + generateThinCallerContent(name, withOverrides, additionalPaths));
|
|
3412
3524
|
}));
|
|
@@ -3578,6 +3690,28 @@ async function runSetup(input) {
|
|
|
3578
3690
|
await deploy.ensureProject({ name: config.name });
|
|
3579
3691
|
}));
|
|
3580
3692
|
print(formatStep(steps[steps.length - 1]));
|
|
3693
|
+
const deployEntry = (config.workflows ?? []).map((e) => typeof e === "string" ? { name: e } : e).find((e) => e.name === "deploy");
|
|
3694
|
+
const previewCfg = deployEntry?.with ? extractPreviewConfig(deployEntry.with) : null;
|
|
3695
|
+
if (previewCfg?.domain && deploy.ensureCustomDomain) {
|
|
3696
|
+
const wildcardDomain = `*.${previewCfg.domain}`;
|
|
3697
|
+
steps.push(await runStep("deployment", `ensureCustomDomain ${wildcardDomain}`, dryRun, async () => {
|
|
3698
|
+
await deploy.ensureCustomDomain(previewCfg.project, wildcardDomain);
|
|
3699
|
+
}));
|
|
3700
|
+
print(formatStep(steps[steps.length - 1]));
|
|
3701
|
+
}
|
|
3702
|
+
if (previewCfg?.domain && loader.has("dns")) {
|
|
3703
|
+
const dns = loader.get("dns");
|
|
3704
|
+
const wildcardDomain = `*.${previewCfg.domain}`;
|
|
3705
|
+
steps.push(await runStep("dns", `upsertRecord ${wildcardDomain}`, dryRun, async () => {
|
|
3706
|
+
await dns.upsertRecord(previewCfg.domain, {
|
|
3707
|
+
type: "CNAME",
|
|
3708
|
+
name: wildcardDomain,
|
|
3709
|
+
content: `${previewCfg.project}.pages.dev`,
|
|
3710
|
+
ttl: 1
|
|
3711
|
+
});
|
|
3712
|
+
}));
|
|
3713
|
+
print(formatStep(steps[steps.length - 1]));
|
|
3714
|
+
}
|
|
3581
3715
|
}
|
|
3582
3716
|
if (loader.has("auth")) {
|
|
3583
3717
|
const auth = loader.get("auth");
|
|
@@ -4480,6 +4614,9 @@ var dependencies_default = "name: Dependencies\n\non: # yamllint disable-line ru
|
|
|
4480
4614
|
//#region src/templates/workflows/deploy.yml
|
|
4481
4615
|
var deploy_default = "name: Deploy\n\non: # yamllint disable-line rule:truthy\n workflow_call:\n inputs:\n type:\n description: \"Type of deployment: docs or storybook\"\n required: true\n type: string\n name:\n description: Repo name prefix used to filter the docs site package (<name>-site); omit to run pnpm -C docs build\n required: false\n type: string\n default: \"\"\n storybook-projects:\n description: >\n JSON array of { \"name\"?, \"workingDir\", \"outputDir\"? } objects for storybook deploys.\n Each is built via `pnpm -C <workingDir> build:storybook`. If \"name\" is provided the\n output is placed under `sandbox/<name>/`; omit \"name\" for single-repo deploys and the\n output lands directly in `sandbox/`.\n type: string\n required: false\n default: \"[]\"\n build-script:\n description: pnpm script that builds the Storybook static output (single storybook, type:storybook only)\n type: string\n required: false\n default: build:storybook\n output-dir:\n description: Directory where Storybook writes its static output (single storybook, type:storybook only)\n type: string\n required: false\n default: storybook-static\n\njobs:\n deploy:\n name: Deploy\n runs-on: ubuntu-latest\n permissions:\n contents: read\n pages: write\n id-token: write\n environment:\n name: github-pages\n url: ${{ steps.deployment.outputs.page_url }}\n steps:\n - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0\n name: Checkout repository\n\n - uses: theholocron/.github/.github/actions/setup@main\n name: Setup\n\n - name: Build docs site\n if: ${{ inputs.type == 'docs' && inputs.name != '' }}\n env:\n SITE_NAME: ${{ inputs.name }}\n run: pnpm --filter @theholocron/\"$SITE_NAME\"-site build\n\n - name: Build docs site\n if: ${{ inputs.type == 'docs' && inputs.name == '' }}\n run: pnpm -C docs build\n\n - name: Build Storybook projects\n if: ${{ inputs.storybook-projects != '[]' }}\n env:\n PROJECTS: ${{ inputs.storybook-projects }}\n run: |\n echo \"$PROJECTS\" | jq -c '.[]' | while IFS= read -r project; do\n workingDir=$(echo \"$project\" | jq -r '.workingDir')\n pnpm -C \"$workingDir\" build:storybook\n done\n\n - name: Build Storybook\n if: ${{ inputs.type == 'storybook' && inputs.storybook-projects == '[]' }}\n env:\n BUILD_SCRIPT: ${{ inputs.build-script }}\n run: pnpm run \"$BUILD_SCRIPT\"\n\n - name: Assemble site\n env:\n DEPLOY_TYPE: ${{ inputs.type }}\n PROJECTS: ${{ inputs.storybook-projects }}\n STORYBOOK_OUTPUT_DIR: ${{ inputs.output-dir }}\n run: |\n mkdir -p _site\n if [ \"$DEPLOY_TYPE\" = \"docs\" ]; then\n cp -r docs/dist/. _site/\n fi\n if [ \"$PROJECTS\" != \"[]\" ]; then\n echo \"$PROJECTS\" | jq -c '.[]' | while IFS= read -r project; do\n name=$(echo \"$project\" | jq -r '.name // \"\"')\n workingDir=$(echo \"$project\" | jq -r '.workingDir')\n outputDir=$(echo \"$project\" | jq -r '.outputDir // \"storybook-static\"')\n if [ -n \"$name\" ]; then\n target=\"_site/sandbox/${name}\"\n else\n target=\"_site/sandbox\"\n fi\n mkdir -p \"$target\"\n cp -r \"${workingDir}/${outputDir}/.\" \"$target/\"\n done\n elif [ \"$DEPLOY_TYPE\" = \"storybook\" ]; then\n mkdir -p _site/sandbox\n cp -r \"${STORYBOOK_OUTPUT_DIR}/.\" _site/sandbox/\n fi\n\n - uses: actions/upload-pages-artifact@56afc609e74202658d3ffba0e8f6dda462b719fa # v3.0.1\n name: Upload pages artifact\n with:\n path: _site\n\n - uses: actions/deploy-pages@d6db90164ac5ed86f2b6aed7e0febac5b3c0c03e # v4.0.5\n id: deployment\n name: Deploy to GitHub Pages\n";
|
|
4482
4616
|
//#endregion
|
|
4617
|
+
//#region src/templates/workflows/deploy-preview.yml
|
|
4618
|
+
var deploy_preview_default = "name: Deploy Preview\n\non: # yamllint disable-line rule:truthy\n workflow_call:\n inputs:\n type:\n description: \"Type of deployment: docs or storybook\"\n required: true\n type: string\n name:\n description: Repo name prefix used to filter the docs site package (<name>-site); omit to run pnpm -C docs build\n required: false\n type: string\n default: \"\"\n storybook-projects:\n description: >\n JSON array of { \"name\"?, \"workingDir\", \"outputDir\"? } objects for storybook deploys.\n Each is built via `pnpm -C <workingDir> build:storybook`. If \"name\" is provided the\n output is placed under `sandbox/<name>/`; omit \"name\" for single-repo deploys and the\n output lands directly in `sandbox/`.\n type: string\n required: false\n default: \"[]\"\n build-script:\n description: pnpm script that builds the Storybook static output (single storybook, type:storybook only)\n type: string\n required: false\n default: build:storybook\n output-dir:\n description: Directory where Storybook writes its static output (single storybook, type:storybook only)\n type: string\n required: false\n default: storybook-static\n cloudflare-project:\n description: >\n Cloudflare Pages project name. Falls back to the CLOUDFLARE_PAGES_PROJECT\n org variable when omitted — set that variable once and all repos get previews\n without per-repo config.\n required: false\n type: string\n default: \"\"\n\njobs:\n deploy-preview:\n name: Deploy Preview\n runs-on: ubuntu-latest\n permissions:\n contents: read\n pull-requests: write\n steps:\n - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0\n name: Checkout repository\n\n - uses: theholocron/.github/.github/actions/setup@main\n name: Setup\n\n - name: Build docs site\n if: ${{ inputs.type == 'docs' && inputs.name != '' }}\n env:\n SITE_NAME: ${{ inputs.name }}\n run: pnpm --filter @theholocron/\"$SITE_NAME\"-site build\n\n - name: Build docs site\n if: ${{ inputs.type == 'docs' && inputs.name == '' }}\n run: pnpm -C docs build\n\n - name: Build Storybook projects\n if: ${{ inputs.storybook-projects != '[]' }}\n env:\n PROJECTS: ${{ inputs.storybook-projects }}\n run: |\n echo \"$PROJECTS\" | jq -c '.[]' | while IFS= read -r project; do\n workingDir=$(echo \"$project\" | jq -r '.workingDir')\n pnpm -C \"$workingDir\" build:storybook\n done\n\n - name: Build Storybook\n if: ${{ inputs.type == 'storybook' && inputs.storybook-projects == '[]' }}\n env:\n BUILD_SCRIPT: ${{ inputs.build-script }}\n run: pnpm run \"$BUILD_SCRIPT\"\n\n - name: Assemble site\n env:\n DEPLOY_TYPE: ${{ inputs.type }}\n PROJECTS: ${{ inputs.storybook-projects }}\n STORYBOOK_OUTPUT_DIR: ${{ inputs.output-dir }}\n run: |\n mkdir -p _site\n if [ \"$DEPLOY_TYPE\" = \"docs\" ]; then\n cp -r docs/dist/. _site/\n fi\n if [ \"$PROJECTS\" != \"[]\" ]; then\n echo \"$PROJECTS\" | jq -c '.[]' | while IFS= read -r project; do\n name=$(echo \"$project\" | jq -r '.name // \"\"')\n workingDir=$(echo \"$project\" | jq -r '.workingDir')\n outputDir=$(echo \"$project\" | jq -r '.outputDir // \"storybook-static\"')\n if [ -n \"$name\" ]; then\n target=\"_site/sandbox/${name}\"\n else\n target=\"_site/sandbox\"\n fi\n mkdir -p \"$target\"\n cp -r \"${workingDir}/${outputDir}/.\" \"$target/\"\n done\n elif [ \"$DEPLOY_TYPE\" = \"storybook\" ]; then\n mkdir -p _site/sandbox\n cp -r \"${STORYBOOK_OUTPUT_DIR}/.\" _site/sandbox/\n fi\n\n - uses: cloudflare/pages-action@f0a1cd58cd66095dee69bfa18fa5efd1dde93bca # v1.5.0\n name: Deploy to Cloudflare Pages\n # Skip when neither the per-repo input nor the org variable is set.\n if: ${{ inputs.cloudflare-project != '' || vars.CLOUDFLARE_PAGES_PROJECT != '' }}\n with:\n apiToken: ${{ secrets.CLOUDFLARE_API_TOKEN }}\n accountId: ${{ vars.CLOUDFLARE_ACCOUNT_ID }}\n projectName: ${{ inputs.cloudflare-project || vars.CLOUDFLARE_PAGES_PROJECT }}\n directory: _site\n gitHubToken: ${{ secrets.GITHUB_TOKEN }}\n # Branch name scoped to repo+PR so the wildcard custom domain\n # *.preview.theholocron.dev resolves to <repo>-pr-<n>.preview.theholocron.dev\n branch: ${{ github.event.repository.name }}-pr-${{ github.event.pull_request.number }}\n";
|
|
4619
|
+
//#endregion
|
|
4483
4620
|
//#region src/templates/workflows/greetings.yml
|
|
4484
4621
|
var greetings_default = "name: Greetings\n\non: # yamllint disable-line rule:truthy\n workflow_call:\n\njobs:\n greeting:\n name: Greet first-time contributors\n permissions:\n issues: write\n pull-requests: write\n runs-on: ubuntu-latest\n timeout-minutes: 5\n # Group by the issue/PR number so duplicate events don't race each other.\n steps:\n - uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0\n name: Greet on first contribution\n with:\n script: |\n // Only greet on the initial open — ignore synchronize, reopened, etc.\n if (context.payload.action !== 'opened') return;\n\n const actor = context.actor;\n const { owner, repo } = context.repo;\n\n // Payload inspection is more reliable than context.eventName for detecting\n // whether this is an issue vs. PR event — works regardless of how GitHub\n // propagates event names through workflow_call chains.\n const isIssue = !!context.payload.issue && !context.payload.pull_request;\n // listForRepo returns both issues and PRs (GitHub treats PRs as issues),\n // sorted newest-first. Filter by type to track first-issue and first-PR\n // independently, and avoid search-index eventual-consistency lag.\n const { data: recent } = await github.rest.issues.listForRepo({\n owner, repo,\n creator: actor,\n state: 'all',\n per_page: 100\n });\n\n const sameType = recent.filter(item =>\n isIssue ? !item.pull_request : !!item.pull_request\n );\n\n if (sameType.length !== 1) return;\n const body = isIssue\n ? `Hey @${actor}!\\n\\nWe really appreciate you taking the time to report an issue. The collaborators on this project attempt to help as many people as possible, but we are a limited number of volunteers, so it is possible that this will not be addressed as swiftly.\\n\\nYour patience is much appreciated and we will get back to you as quickly as possible.`\n : `Hey @${actor}!\\n\\nWe really appreciate you taking the time to help out with this PR. The collaborators on this project attempt to help as many people as possible, but we are a limited number of volunteers, so it is possible that this will not be addressed as swiftly.\\n\\nYour patience is much appreciated and we will get back to you as quickly as possible.`;\n\n await github.rest.issues.createComment({\n owner,\n repo,\n issue_number: context.issue.number,\n body\n });\n";
|
|
4485
4622
|
//#endregion
|
|
@@ -4508,7 +4645,7 @@ var sync_broadcast_default = "name: Sync Broadcast\n\non: # yamllint disable-lin
|
|
|
4508
4645
|
var sync_github_default = "name: Sync GitHub Templates\n\n# Builds the holocron CLI from source and pushes updated workflow templates\n# and composite actions to downstream .github repos. Runs whenever the\n# template source files change on main or alpha.\n#\n# Secrets required:\n# HOLOCRON_SYNC_TOKEN — fine-grained PAT (resource owner: org) with:\n# Actions: Read and write (dispatch workflow runs via gh workflow run)\n# Contents: Read and write (git trees, blobs, refs)\n# Pull requests: Read and write (open sync PR)\n# Workflows: Read and write (write .github/workflows/*.yml)\n\non: # yamllint disable-line rule:truthy\n workflow_call:\n inputs:\n primary-repo:\n description: >\n Primary .github repo — receives composite actions, reusable workflows,\n and thin-caller templates. Requires a PR (branch protection assumed).\n type: string\n required: false\n default: theholocron/.github\n secondary-repos:\n description: >\n Space-separated list of secondary repos (reusable workflows + thin\n callers only, no composite actions). Changes are delivered via pull\n request, same as the primary repo.\n type: string\n required: false\n default: \"\"\n sync-branch:\n description: Branch name used for the primary and secondary repo PRs\n type: string\n required: false\n default: chore/sync-templates\n secrets:\n HOLOCRON_SYNC_TOKEN:\n required: true\n HOLOCRON_READ_TOKEN:\n description: >\n Fine-grained PAT for read-only GitHub API calls (e.g. resolving git\n committer identity via `gh api user`). Falls back to HOLOCRON_SYNC_TOKEN.\n required: false\n GH_TOKEN:\n description: >\n Generic GitHub token fallback for `gh` CLI calls. Used when neither\n HOLOCRON_READ_TOKEN nor HOLOCRON_SYNC_TOKEN is set.\n required: false\n TURBO_TOKEN:\n required: false\n\njobs:\n sync:\n name: Sync templates\n runs-on: ubuntu-latest\n timeout-minutes: 15\n permissions:\n contents: read\n env:\n TURBO_TOKEN: ${{ secrets.TURBO_TOKEN }}\n TURBO_TEAM: ${{ vars.TURBO_TEAM }}\n steps:\n - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0\n name: Checkout repository\n\n - uses: theholocron/.github/.github/actions/setup@main\n name: Setup\n\n - run: pnpm build\n name: Build CLI\n\n - name: Cache actionlint\n id: cache-actionlint\n uses: actions/cache@0057852bfaa89a56745cba8c7296529d2fc39830 # v4.3.0\n with:\n path: /tmp/actionlint\n key: actionlint-v1.7.7-linux-amd64\n\n - name: Download actionlint\n if: steps.cache-actionlint.outputs.cache-hit != 'true'\n run: |\n curl -fsSL https://github.com/rhysd/actionlint/releases/download/v1.7.7/actionlint_1.7.7_linux_amd64.tar.gz \\\n | tar -xz -C /tmp actionlint\n\n - name: Validate generated workflows\n run: |\n node packages/cli/dist/cli.mjs sync-github \\\n --repo \"$PRIMARY_REPO\" \\\n --output-dir /tmp/sync-validate\n /tmp/actionlint /tmp/sync-validate/.github/workflows/*.yml\n env:\n PRIMARY_REPO: ${{ inputs.primary-repo }}\n\n - name: Sync primary repo (PR)\n run: |\n GIT_NAME=$(gh api user --jq .name 2>/dev/null || echo \"github-actions[bot]\")\n GIT_EMAIL=$(gh api user --jq '\"\\(.id)+\\(.login)@users.noreply.github.com\"' 2>/dev/null || echo \"41898282+github-actions[bot]@users.noreply.github.com\")\n COMMIT_MSG=\"chore: sync from theholocron/holocron\"$'\\n\\n'\"Signed-off-by: $GIT_NAME <$GIT_EMAIL>\"\n node packages/cli/dist/cli.mjs sync-github \\\n --repo \"$PRIMARY_REPO\" \\\n --branch \"$SYNC_BRANCH\" \\\n --pr \\\n --message \"$COMMIT_MSG\"\n GH_TOKEN=\"$HOLOCRON_SYNC_TOKEN\" gh pr merge --auto --squash \\\n --repo \"$PRIMARY_REPO\" \"$SYNC_BRANCH\" 2>/dev/null || true\n env:\n GH_TOKEN: ${{ secrets.HOLOCRON_READ_TOKEN || secrets.HOLOCRON_SYNC_TOKEN || secrets.GH_TOKEN }}\n HOLOCRON_SYNC_TOKEN: ${{ secrets.HOLOCRON_SYNC_TOKEN }}\n PRIMARY_REPO: ${{ inputs.primary-repo }}\n SYNC_BRANCH: ${{ inputs.sync-branch }}\n\n - name: Sync secondary repos (PR)\n if: ${{ inputs.secondary-repos != '' }}\n run: |\n GIT_NAME=$(gh api user --jq .name 2>/dev/null || echo \"github-actions[bot]\")\n GIT_EMAIL=$(gh api user --jq '\"\\(.id)+\\(.login)@users.noreply.github.com\"' 2>/dev/null || echo \"41898282+github-actions[bot]@users.noreply.github.com\")\n COMMIT_MSG=\"chore: sync from theholocron/holocron\"$'\\n\\n'\"Signed-off-by: $GIT_NAME <$GIT_EMAIL>\"\n for repo in $SECONDARY_REPOS; do\n node packages/cli/dist/cli.mjs sync-github \\\n --repo \"$repo\" \\\n --branch \"$SYNC_BRANCH\" \\\n --pr \\\n --message \"$COMMIT_MSG\"\n GH_TOKEN=\"$HOLOCRON_SYNC_TOKEN\" gh pr merge --auto --squash \\\n --repo \"$repo\" \"$SYNC_BRANCH\" 2>/dev/null || true\n done\n env:\n GH_TOKEN: ${{ secrets.HOLOCRON_READ_TOKEN || secrets.HOLOCRON_SYNC_TOKEN || secrets.GH_TOKEN }}\n HOLOCRON_SYNC_TOKEN: ${{ secrets.HOLOCRON_SYNC_TOKEN }}\n SECONDARY_REPOS: ${{ inputs.secondary-repos }}\n SYNC_BRANCH: ${{ inputs.sync-branch }}\n";
|
|
4509
4646
|
//#endregion
|
|
4510
4647
|
//#region src/templates/workflows/tag.yml
|
|
4511
|
-
var tag_default = "name: Tag\n\n# Release Please — fully automated tag and GitHub Release from Conventional Commits.\n# No package.json or npm publishing required. Operates in \"simple\" mode by default:\n# analyzes commits since the last tag, maintains a rolling Release PR, and creates\n# a tag + GitHub Release when that PR is merged.\n#\n# The calling repo must have two files at the root:\n# release-please-config.json — declares packages and release-type\n# .release-please-manifest.json — tracks the current version\n\non: # yamllint disable-line rule:truthy\n workflow_call:\n inputs:\n release-type:\n description: Release Please release type (simple, node, python, etc.)\n type: string\n required: false\n default: simple\n config-file:\n description: Path to release-please-config.json\n type: string\n required: false\n default: release-please-config.json\n manifest-file:\n description: Path to .release-please-manifest.json\n type: string\n required: false\n default: .release-please-manifest.json\n\njobs:\n tag:\n name: Tag release\n permissions:\n contents: write\n pull-requests: write\n runs-on: ubuntu-latest\n timeout-minutes: 10\n steps:\n - uses: google-github-actions/release-please-action@e4dc86ba9405554aeba3c6bb2d169500e7d3b4ee # v4.1.1\n name: Run Release Please\n with:\n release-type: ${{ inputs.release-type }}\n config-file: ${{ inputs.config-file }}\n manifest-file: ${{ inputs.manifest-file }}\n";
|
|
4648
|
+
var tag_default = "name: Tag\n\n# Release Please — fully automated tag and GitHub Release from Conventional Commits.\n# No package.json or npm publishing required. Operates in \"simple\" mode by default:\n# analyzes commits since the last tag, maintains a rolling Release PR, and creates\n# a tag + GitHub Release when that PR is merged.\n#\n# The calling repo must have two files at the root:\n# release-please-config.json — declares packages and release-type\n# .release-please-manifest.json — tracks the current version\n\non: # yamllint disable-line rule:truthy\n # Self-trigger: when this workflow lives in theholocron/.github itself,\n # push to main runs Release Please for that repo's own releases.\n push:\n branches:\n - main\n workflow_call:\n inputs:\n release-type:\n description: Release Please release type (simple, node, python, etc.)\n type: string\n required: false\n default: simple\n config-file:\n description: Path to release-please-config.json\n type: string\n required: false\n default: release-please-config.json\n manifest-file:\n description: Path to .release-please-manifest.json\n type: string\n required: false\n default: .release-please-manifest.json\n\njobs:\n tag:\n name: Tag release\n permissions:\n contents: write\n pull-requests: write\n runs-on: ubuntu-latest\n timeout-minutes: 10\n steps:\n - uses: google-github-actions/release-please-action@e4dc86ba9405554aeba3c6bb2d169500e7d3b4ee # v4.1.1\n name: Run Release Please\n with:\n release-type: ${{ inputs.release-type }}\n config-file: ${{ inputs.config-file }}\n manifest-file: ${{ inputs.manifest-file }}\n";
|
|
4512
4649
|
//#endregion
|
|
4513
4650
|
//#region src/templates/workflows/test.yml
|
|
4514
4651
|
var test_default = "name: Test\n\non: # yamllint disable-line rule:truthy\n workflow_call:\n inputs:\n run-unit:\n description: Run unit tests with coverage (disable for UI-only repos that use Storybook testing exclusively)\n type: boolean\n required: false\n default: true\n run-storybook:\n description: Run Storybook vitest interaction tests (requires .storybook/ setup)\n type: boolean\n required: false\n default: false\n run-interaction:\n description: Run Storybook interaction and accessibility tests with Playwright\n type: boolean\n required: false\n default: false\n run-chromatic:\n description: Publish Storybook to Chromatic for visual regression testing\n type: boolean\n required: false\n default: false\n chromatic-projects:\n description: >\n JSON array of Chromatic projects to build, one matrix job per entry.\n Each entry: { \"tokenName\": \"WEB\", \"workingDir\": \"apps/web\", \"buildScript\": \"build:storybook\" }.\n tokenName maps to secret CHROMATIC_PROJECT_TOKEN_<TOKENNAME>; use \"default\"\n (or omit for the legacy empty-string form) to use the bare CHROMATIC_PROJECT_TOKEN\n secret for single-project repos.\n buildScript defaults to \"build:storybook\" when omitted.\n Default runs a single job from the repo root using CHROMATIC_PROJECT_TOKEN.\n type: string\n required: false\n default: '[{\"tokenName\":\"default\",\"workingDir\":\".\",\"buildScript\":\"build:storybook\"}]'\n run-user-flow:\n description: Run Cypress E2E user-flow tests (requires cypress.config.*)\n type: boolean\n required: false\n default: false\n wait-on-url:\n description: URL to wait for before running Cypress tests (default is Vite dev server; override for non-Vite stacks e.g. http://localhost:3000 for Next.js)\n type: string\n required: false\n default: \"http://localhost:5173\"\n secrets:\n CHROMATIC_PROJECT_TOKEN:\n required: false\n CYPRESS_RECORD_KEY:\n required: false\n TURBO_TOKEN:\n required: false\n\njobs:\n unit:\n name: Run tests and collect coverage\n if: ${{ inputs.run-unit }}\n permissions:\n contents: read\n id-token: write\n runs-on: ubuntu-latest\n timeout-minutes: 15\n env:\n TURBO_TOKEN: ${{ secrets.TURBO_TOKEN }}\n TURBO_TEAM: ${{ vars.TURBO_TEAM }}\n steps:\n - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0\n name: Checkout repository\n\n - uses: theholocron/.github/.github/actions/setup@main\n name: Setup\n\n - run: pnpm test:coverage\n name: Run tests with coverage\n\n - uses: codecov/codecov-action@fb8b3582c8e4def4969c97caa2f19720cb33a72f # v7.0.0\n name: Upload coverage to Codecov\n with:\n use_oidc: true\n\n - uses: codecov/test-results-action@0fa95f0e1eeaafde2c782583b36b28ad0d8c77d3 # v1\n name: Upload test results to Codecov\n if: ${{ !cancelled() }}\n with:\n use_oidc: true\n files: '**/test-report.junit.xml'\n\n storybook:\n name: Run Storybook interaction tests\n if: ${{ inputs.run-storybook }}\n permissions:\n contents: read\n id-token: write\n runs-on: ubuntu-latest\n timeout-minutes: 15\n env:\n TURBO_TOKEN: ${{ secrets.TURBO_TOKEN }}\n TURBO_TEAM: ${{ vars.TURBO_TEAM }}\n steps:\n - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0\n name: Checkout repository\n\n - uses: theholocron/.github/.github/actions/setup@main\n name: Setup\n\n - run: pnpm exec playwright install chromium --with-deps\n name: Install Playwright\n\n - run: pnpm test:storybook\n name: Run Storybook tests\n\n - uses: codecov/codecov-action@fb8b3582c8e4def4969c97caa2f19720cb33a72f # v7.0.0\n name: Upload coverage to Codecov\n with:\n use_oidc: true\n\n - uses: codecov/test-results-action@0fa95f0e1eeaafde2c782583b36b28ad0d8c77d3 # v1\n name: Upload test results to Codecov\n if: ${{ !cancelled() }}\n with:\n use_oidc: true\n files: '**/test-report.junit.xml'\n\n visual-and-composition:\n name: Test Visual and Composition (${{ matrix.project.tokenName }})\n if: ${{ inputs.run-chromatic }}\n strategy:\n fail-fast: false\n matrix:\n project: ${{ fromJSON(inputs.chromatic-projects) }}\n permissions:\n contents: read\n statuses: write\n runs-on: ubuntu-latest\n steps:\n - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0\n name: Checkout repository\n with:\n fetch-depth: 0\n\n - uses: theholocron/.github/.github/actions/setup@main\n name: Setup\n\n - uses: chromaui/action@14cfaef73576e69f95f47f60058063f46ca38719 # v18\n name: Publish to Chromatic\n with:\n projectToken: ${{ (matrix.project.tokenName == 'default' || matrix.project.tokenName == '') && secrets.CHROMATIC_PROJECT_TOKEN || secrets[format('CHROMATIC_PROJECT_TOKEN_{0}', matrix.project.tokenName)] }}\n token: ${{ github.token }}\n buildScriptName: ${{ matrix.project.buildScript || 'build:storybook' }}\n workingDir: ${{ matrix.project.workingDir || '.' }}\n storybookBaseDir: ${{ matrix.project.storybookBaseDir || '' }}\n untraced: ${{ matrix.project.untraced || '' }}\n onlyStoryFiles: ${{ matrix.project.onlyStoryFiles || '' }}\n exitZeroOnChanges: ${{ matrix.project.exitZeroOnChanges || false }}\n\n interaction-and-accessibility:\n name: Test Interactions and Accessibility\n if: ${{ inputs.run-interaction }}\n permissions:\n contents: read\n runs-on: ubuntu-latest\n timeout-minutes: 15\n env:\n TURBO_TOKEN: ${{ secrets.TURBO_TOKEN }}\n TURBO_TEAM: ${{ vars.TURBO_TEAM }}\n steps:\n - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0\n name: Checkout repository\n\n - uses: theholocron/.github/.github/actions/setup@main\n name: Setup\n\n - run: pnpm exec playwright install --with-deps\n name: Install Playwright\n\n - run: pnpm test:storybook\n name: Run interaction and accessibility tests\n\n user-flow:\n name: Test User Flow\n if: ${{ inputs.run-user-flow }}\n permissions:\n contents: read\n runs-on: ubuntu-latest\n timeout-minutes: 15\n strategy:\n fail-fast: false\n matrix:\n containers: [1, 2]\n env:\n TURBO_TOKEN: ${{ secrets.TURBO_TOKEN }}\n TURBO_TEAM: ${{ vars.TURBO_TEAM }}\n steps:\n - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0\n name: Checkout repository\n\n - uses: theholocron/.github/.github/actions/setup@main\n name: Setup\n\n - run: pnpm exec cypress install\n name: Install Cypress binary\n\n - uses: cypress-io/github-action@1052aa98bbbe4f55210f844878213c07d9c8c399 # v6.7.13\n name: Cypress run\n with:\n start: pnpm dev\n wait-on: ${{ inputs.wait-on-url }}\n record: true\n parallel: true\n env:\n CYPRESS_RECORD_KEY: ${{ secrets.CYPRESS_RECORD_KEY }}\n GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}\n";
|
|
@@ -4537,6 +4674,7 @@ const REUSABLE_WORKFLOWS = {
|
|
|
4537
4674
|
codeql: codeql_default,
|
|
4538
4675
|
dependencies: dependencies_default,
|
|
4539
4676
|
deploy: deploy_default,
|
|
4677
|
+
"deploy-preview": deploy_preview_default,
|
|
4540
4678
|
greetings: greetings_default,
|
|
4541
4679
|
lint: lint_default,
|
|
4542
4680
|
"post-release": post_release_default,
|
|
@@ -4635,6 +4773,18 @@ function parseWorkflowsFromTs(source) {
|
|
|
4635
4773
|
for (const entry of explicitEntries) merged.set(entry.name, entry);
|
|
4636
4774
|
return [...merged.values()];
|
|
4637
4775
|
}
|
|
4776
|
+
/**
|
|
4777
|
+
* Extract org name and docs domain from a `holocron.config.ts` source string.
|
|
4778
|
+
* Used to resolve `preview: true` to `{ project: "<org>-preview", domain: "preview.<docsDomain>" }`.
|
|
4779
|
+
*/
|
|
4780
|
+
function parseOrgContextFromTs(source) {
|
|
4781
|
+
const orgMatch = source.match(/\borg\s*:\s*["']([^"']+)["']/);
|
|
4782
|
+
const domainMatch = source.match(/\bdocs\s*:[^}]*?domain\s*:\s*["']([^"']+)["']/s);
|
|
4783
|
+
return {
|
|
4784
|
+
org: orgMatch?.[1],
|
|
4785
|
+
docsDomain: domainMatch?.[1]
|
|
4786
|
+
};
|
|
4787
|
+
}
|
|
4638
4788
|
function reusableHeader(source) {
|
|
4639
4789
|
return [
|
|
4640
4790
|
`# AUTO-GENERATED — do not edit in theholocron/.github directly.`,
|
|
@@ -4645,7 +4795,7 @@ function reusableHeader(source) {
|
|
|
4645
4795
|
``
|
|
4646
4796
|
].join("\n");
|
|
4647
4797
|
}
|
|
4648
|
-
function buildBatch(repo, allowedWorkflows, withOverrides) {
|
|
4798
|
+
function buildBatch(repo, allowedWorkflows, withOverrides, orgContext) {
|
|
4649
4799
|
const files = [];
|
|
4650
4800
|
const isPrimaryGithubRepo = repo === DEFAULT_REPO;
|
|
4651
4801
|
if (isPrimaryGithubRepo) for (const [name, content] of Object.entries(ACTIONS)) files.push({
|
|
@@ -4669,9 +4819,22 @@ function buildBatch(repo, allowedWorkflows, withOverrides) {
|
|
|
4669
4819
|
});
|
|
4670
4820
|
}
|
|
4671
4821
|
} else for (const name of Object.keys(REUSABLE_WORKFLOWS)) {
|
|
4822
|
+
if (name === "deploy-preview") continue;
|
|
4672
4823
|
if (allowedWorkflows && !allowedWorkflows.has(name)) continue;
|
|
4673
4824
|
const rawWith = withOverrides?.get(name);
|
|
4674
|
-
const
|
|
4825
|
+
const normalizedWith = rawWith ? normalizeWorkflowWith(rawWith) : void 0;
|
|
4826
|
+
const additionalPaths = name === "deploy" && rawWith ? deriveDeployPaths(rawWith) : void 0;
|
|
4827
|
+
if (name === "deploy" && rawWith) {
|
|
4828
|
+
const previewCfg = extractPreviewConfig(rawWith, orgContext);
|
|
4829
|
+
if (previewCfg) {
|
|
4830
|
+
files.push({
|
|
4831
|
+
path: `.github/workflows/deploy.yml`,
|
|
4832
|
+
content: workflowHeader() + generateCombinedDeployContent(normalizedWith, additionalPaths, previewCfg)
|
|
4833
|
+
});
|
|
4834
|
+
continue;
|
|
4835
|
+
}
|
|
4836
|
+
}
|
|
4837
|
+
const content = generateThinCallerContent(name, normalizedWith, additionalPaths);
|
|
4675
4838
|
if (!content) continue;
|
|
4676
4839
|
files.push({
|
|
4677
4840
|
path: `.github/workflows/${name}.yml`,
|
|
@@ -4751,6 +4914,7 @@ async function runSyncGithub(input) {
|
|
|
4751
4914
|
}
|
|
4752
4915
|
let allowedWorkflows;
|
|
4753
4916
|
let withOverrides;
|
|
4917
|
+
let orgContext;
|
|
4754
4918
|
if (repo !== DEFAULT_REPO) try {
|
|
4755
4919
|
let entries = [];
|
|
4756
4920
|
try {
|
|
@@ -4760,7 +4924,9 @@ async function runSyncGithub(input) {
|
|
|
4760
4924
|
if (!(err instanceof ProviderApiError) || err.status !== 404) throw err;
|
|
4761
4925
|
try {
|
|
4762
4926
|
const data = await client.git.getContents(repo, "holocron.config.ts");
|
|
4763
|
-
|
|
4927
|
+
const source = Buffer.from(data.content.replace(/\n/g, ""), "base64").toString("utf8");
|
|
4928
|
+
entries = parseWorkflowsFromTs(source);
|
|
4929
|
+
orgContext = parseOrgContextFromTs(source);
|
|
4764
4930
|
} catch {}
|
|
4765
4931
|
}
|
|
4766
4932
|
if (entries.length > 0) {
|
|
@@ -4769,7 +4935,7 @@ async function runSyncGithub(input) {
|
|
|
4769
4935
|
if (overrideEntries.length > 0) withOverrides = new Map(overrideEntries);
|
|
4770
4936
|
}
|
|
4771
4937
|
} catch {}
|
|
4772
|
-
const batch = buildBatch(repo, allowedWorkflows, withOverrides);
|
|
4938
|
+
const batch = buildBatch(repo, allowedWorkflows, withOverrides, orgContext);
|
|
4773
4939
|
let created = 0;
|
|
4774
4940
|
let updated = 0;
|
|
4775
4941
|
let unchanged = 0;
|