Skip to content

Commit c77ea94

Browse files
committed
1 parent 81c36ce commit c77ea94

File tree

5 files changed

+377
-0
lines changed

5 files changed

+377
-0
lines changed

examples.md

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -174,6 +174,22 @@ Publish site to Vercel, setting the project name
174174
```bash
175175
hax site site:vercel --domain my-project-name --y
176176
```
177+
Setup GitHub Actions deployment workflow (deploys automatically on git push)
178+
```bash
179+
hax site setup:github-actions
180+
```
181+
Setup GitHub Actions deployment workflow, overwrite existing file
182+
```bash
183+
hax site setup:github-actions --y
184+
```
185+
Setup GitLab CI deployment pipeline (deploys automatically on git push)
186+
```bash
187+
hax site setup:gitlab-ci
188+
```
189+
Setup GitLab CI deployment pipeline, overwrite existing file
190+
```bash
191+
hax site setup:gitlab-ci --y
192+
```
177193
Print out the recipe used in building the current site
178194
```bash
179195
hax site recipe:read

src/create.js

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -86,6 +86,13 @@ async function main() {
8686
.option('--custom-theme-name <char>', 'custom theme name')
8787
.option('--custom-theme-template <char>', 'custom theme template; (options: base, polaris-flex, polaris-sidebar)')
8888

89+
// options for rsync
90+
.option('--source <char>', 'rsync source directory or remote path')
91+
.option('--destination <char>', 'rsync destination directory or remote path')
92+
.option('--exclude <char>', 'comma-separated patterns to exclude from rsync')
93+
.option('--dry-run', 'perform rsync dry run')
94+
.option('--delete', 'delete extraneous files from destination')
95+
8996
// options for party
9097
.option('--repos <char...>', 'repositories to clone')
9198

@@ -174,6 +181,11 @@ async function main() {
174181
.option('--recipe <char>', 'path to recipe file')
175182
.option('--custom-theme-name <char>', 'custom theme name')
176183
.option('--custom-theme-template <char>', 'custom theme template (options: base, polaris-flex, polaris-sidebar)')
184+
.option('--source <char>', 'rsync source directory or remote path')
185+
.option('--destination <char>', 'rsync destination directory or remote path')
186+
.option('--exclude <char>', 'comma-separated patterns to exclude from rsync')
187+
.option('--dry-run', 'perform rsync dry run')
188+
.option('--delete', 'delete extraneous files from destination')
177189
.version(packageJson.version);
178190
let siteNodeOps = siteNodeOperations();
179191
for (var i in siteNodeOps) {

src/lib/programs/site.js

Lines changed: 224 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,13 @@ exec('vercel --version', error => {
5050
}
5151
});
5252

53+
var sysRsync = true;
54+
exec('rsync --version', error => {
55+
if (error) {
56+
sysRsync = false;
57+
}
58+
});
59+
5360
const siteRecipeFile = 'create-cli.recipe';
5461
const siteLoggingName = 'cli';
5562
const logLevels = {};
@@ -99,9 +106,12 @@ export function siteActions() {
99106
{ value: 'site:md', label: "Full site as Markdown"},
100107
{ value: 'site:schema', label: "Full site as HAXElementSchema"},
101108
{ value: 'site:sync', label: "Sync git repo"},
109+
{ value: 'site:rsync', label: "Rsync site to remote/local directory"},
102110
{ value: 'site:surge', label: "Publish site to Surge.sh"},
103111
{ value: 'site:netlify', label: "Publish site to Netlify"},
104112
{ value: 'site:vercel', label: "Publish site to Vercel"},
113+
{ value: 'setup:github-actions', label: "Setup GitHub Actions deployment"},
114+
{ value: 'setup:gitlab-ci', label: "Setup GitLab CI deployment"},
105115
{ value: 'recipe:read', label: "Read recipe file" },
106116
{ value: 'recipe:play', label: "Play recipe file" },
107117
{ value: 'issue:general', label: "Issue: Submit an issue or suggestion"},
@@ -751,6 +761,146 @@ export async function siteCommandDetected(commandRun) {
751761
log(e.stderr);
752762
}
753763
break;
764+
case "site:rsync":
765+
try {
766+
if (!sysRsync) {
767+
if (!commandRun.options.quiet) {
768+
p.intro(`${color.bgRed(color.white(` ERROR: rsync not found `))}`);
769+
p.outro(`${color.red('rsync is required but not installed on this system.')}`);
770+
p.outro(`${color.yellow('Install rsync:')}`);
771+
p.outro(`${color.gray(' Ubuntu/Debian: sudo apt install rsync')}`);
772+
p.outro(`${color.gray(' macOS: brew install rsync')}`);
773+
p.outro(`${color.gray(' CentOS/RHEL: sudo yum install rsync')}`);
774+
}
775+
break;
776+
}
777+
778+
let source = commandRun.options.source || activeHaxsite.directory;
779+
let destination = commandRun.options.destination;
780+
let excludePatterns = commandRun.options.exclude ? commandRun.options.exclude.split(',').map(p => p.trim()) : ['node_modules', '.git', '.DS_Store', 'dist', 'build'];
781+
let dryRun = commandRun.options.dryRun || false;
782+
783+
// Interactive prompts if not provided via CLI
784+
if (!commandRun.options.y && !destination) {
785+
let action = await p.select({
786+
message: 'Rsync action:',
787+
options: [
788+
{ value: 'to-remote', label: 'Sync site to remote server' },
789+
{ value: 'to-local', label: 'Sync site to local directory' },
790+
{ value: 'from-remote', label: 'Sync from remote server to site' },
791+
{ value: 'test', label: 'Test sync (dry run)' }
792+
]
793+
});
794+
795+
if (action === 'test') {
796+
dryRun = true;
797+
}
798+
799+
if (action === 'from-remote') {
800+
source = await p.text({
801+
message: 'Source (user@host:/path):',
802+
placeholder: 'user@example.com:/var/www/html',
803+
validate: (value) => {
804+
if (!value) return 'Source is required';
805+
}
806+
});
807+
destination = activeHaxsite.directory;
808+
} else {
809+
destination = await p.text({
810+
message: action === 'to-remote' ? 'Destination (user@host:/path):' : 'Destination directory:',
811+
placeholder: action === 'to-remote' ? 'user@example.com:/var/www/html' : '/backup/location',
812+
validate: (value) => {
813+
if (!value) return 'Destination is required';
814+
}
815+
});
816+
}
817+
818+
let excludeInput = await p.text({
819+
message: 'Exclude patterns (comma-separated):',
820+
placeholder: 'node_modules,.git,.DS_Store,dist,build',
821+
initialValue: 'node_modules,.git,.DS_Store,dist,build'
822+
});
823+
824+
if (excludeInput) {
825+
excludePatterns = excludeInput.split(',').map(p => p.trim());
826+
}
827+
828+
if (!dryRun && action !== 'test') {
829+
dryRun = await p.confirm({
830+
message: 'Perform dry run first?',
831+
initialValue: true
832+
});
833+
}
834+
}
835+
836+
if (!destination) {
837+
if (!commandRun.options.quiet) {
838+
p.intro(`${color.bgRed(color.white(` ERROR: destination required `))}`);
839+
}
840+
break;
841+
}
842+
843+
// Build rsync command
844+
let rsyncArgs = [
845+
'-avz', // archive, verbose, compress
846+
'--progress', // show progress
847+
'--stats' // show stats
848+
];
849+
850+
// Add dry run flag if requested
851+
if (dryRun) {
852+
rsyncArgs.push('--dry-run');
853+
}
854+
855+
// Add exclude patterns
856+
excludePatterns.forEach(pattern => {
857+
rsyncArgs.push('--exclude', pattern);
858+
});
859+
860+
// Add delete flag to mirror source (be careful with this)
861+
if (commandRun.options.delete) {
862+
rsyncArgs.push('--delete');
863+
}
864+
865+
// Add source and destination
866+
// Ensure source ends with / for directory contents
867+
if (!source.endsWith('/') && fs.lstatSync(source).isDirectory()) {
868+
source += '/';
869+
}
870+
871+
rsyncArgs.push(source, destination);
872+
873+
if (!commandRun.options.quiet) {
874+
p.intro(`${dryRun ? color.yellow('🧪 Dry run: ') : color.green('🚀 Running: ')}rsync ${rsyncArgs.join(' ')}`);
875+
}
876+
877+
if (commandRun.options.i && !commandRun.options.quiet) {
878+
// Interactive execution for real-time progress
879+
await interactiveExec('rsync', rsyncArgs);
880+
} else {
881+
// Silent execution
882+
const result = await exec(`rsync ${rsyncArgs.join(' ')}`);
883+
if (!commandRun.options.quiet && result.stdout) {
884+
console.log(result.stdout);
885+
}
886+
if (result.stderr) {
887+
console.error(result.stderr);
888+
}
889+
}
890+
891+
recipe.log(siteLoggingName, commandString(commandRun));
892+
if (!commandRun.options.quiet) {
893+
p.outro(`${color.green('✓')} ${dryRun ? 'Dry run completed' : 'Rsync completed successfully'}`);
894+
}
895+
}
896+
catch(e) {
897+
log(`Rsync error: ${e.message}`, 'error');
898+
if (!commandRun.options.quiet) {
899+
p.intro(`${color.bgRed(color.white(` Rsync Error `))}`);
900+
p.outro(`${color.red('✗')} ${e.message}`);
901+
}
902+
}
903+
break;
754904
case "site:theme":
755905
try {
756906
//theme
@@ -1092,6 +1242,80 @@ export async function siteCommandDetected(commandRun) {
10921242
log(e.stderr);
10931243
}
10941244
break;
1245+
case "setup:github-actions":
1246+
try {
1247+
let s = p.spinner();
1248+
s.start(merlinSays('Setting up GitHub Actions deployment workflow'));
1249+
1250+
// Create .github/workflows directory
1251+
const workflowDir = path.join(activeHaxsite.directory, '.github', 'workflows');
1252+
if (!fs.existsSync(workflowDir)) {
1253+
fs.mkdirSync(workflowDir, { recursive: true });
1254+
}
1255+
1256+
// Copy the workflow file
1257+
const workflowFile = path.join(workflowDir, 'deploy.yml');
1258+
if (fs.existsSync(workflowFile) && !commandRun.options.y) {
1259+
s.stop(merlinSays('GitHub Actions workflow already exists'));
1260+
let overwrite = await p.confirm({
1261+
message: 'GitHub Actions workflow file already exists. Overwrite?',
1262+
initialValue: false
1263+
});
1264+
if (!overwrite) {
1265+
log('Skipped GitHub Actions setup');
1266+
break;
1267+
}
1268+
}
1269+
1270+
await fs.copyFileSync(
1271+
path.join(process.mainModule.path, 'templates/sitedotfiles/_github_workflows_deploy.yml'),
1272+
workflowFile
1273+
);
1274+
1275+
s.stop(merlinSays('GitHub Actions workflow created successfully'));
1276+
if (!commandRun.options.quiet) {
1277+
p.note(`🚀 GitHub Actions workflow has been set up!\n\nNext steps:\n1. Push your changes: ${color.bold('git add . && git commit -m "Add GitHub Actions workflow" && git push')}\n2. Enable GitHub Pages in your repository settings\n3. Select "GitHub Actions" as the source\n4. Your site will automatically deploy on every push to main/master`);
1278+
}
1279+
}
1280+
catch(e) {
1281+
console.log("?");
1282+
log(e.stderr);
1283+
}
1284+
break;
1285+
case "setup:gitlab-ci":
1286+
try {
1287+
let s = p.spinner();
1288+
s.start(merlinSays('Setting up GitLab CI deployment pipeline'));
1289+
1290+
// Copy the GitLab CI file
1291+
const ciFile = path.join(activeHaxsite.directory, '.gitlab-ci.yml');
1292+
if (fs.existsSync(ciFile) && !commandRun.options.y) {
1293+
s.stop(merlinSays('GitLab CI file already exists'));
1294+
let overwrite = await p.confirm({
1295+
message: '.gitlab-ci.yml already exists. Overwrite?',
1296+
initialValue: false
1297+
});
1298+
if (!overwrite) {
1299+
log('Skipped GitLab CI setup');
1300+
break;
1301+
}
1302+
}
1303+
1304+
await fs.copyFileSync(
1305+
path.join(process.mainModule.path, 'templates/sitedotfiles/_gitlab-ci.yml'),
1306+
ciFile
1307+
);
1308+
1309+
s.stop(merlinSays('GitLab CI pipeline created successfully'));
1310+
if (!commandRun.options.quiet) {
1311+
p.note(`🚀 GitLab CI pipeline has been set up!\n\nNext steps:\n1. Push your changes: ${color.bold('git add . && git commit -m "Add GitLab CI pipeline" && git push')}\n2. GitLab Pages will be automatically enabled\n3. Your site will deploy on every push to main/master\n4. Access your site at: ${color.cyan('https://yourusername.gitlab.io/yourproject')}`);
1312+
}
1313+
}
1314+
catch(e) {
1315+
console.log("?");
1316+
log(e.stderr);
1317+
}
1318+
break;
10951319
case "site:file-list":
10961320
let res = new Res();
10971321
await hax.RoutesMap.get.listFiles({query: activeHaxsite.name, filename: commandRun.options.filename}, res);
Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
name: Deploy HAX Site to GitHub Pages
2+
3+
on:
4+
push:
5+
branches: [ main, master ]
6+
pull_request:
7+
branches: [ main, master ]
8+
9+
# Allow one concurrent deployment
10+
concurrency:
11+
group: "pages"
12+
cancel-in-progress: true
13+
14+
# Sets permissions of the GITHUB_TOKEN to allow deployment to GitHub Pages
15+
permissions:
16+
contents: read
17+
pages: write
18+
id-token: write
19+
20+
jobs:
21+
# Build job
22+
build:
23+
runs-on: ubuntu-latest
24+
steps:
25+
- name: Checkout
26+
uses: actions/checkout@v4
27+
28+
- name: Setup Node.js
29+
uses: actions/setup-node@v4
30+
with:
31+
node-version: '20'
32+
cache: 'npm'
33+
34+
- name: Install dependencies
35+
run: npm ci
36+
37+
- name: Setup Pages
38+
id: pages
39+
uses: actions/configure-pages@v4
40+
41+
- name: Build HAX site
42+
run: |
43+
# HAX sites are typically static and ready to serve
44+
# No build step needed for basic HAX sites
45+
echo "HAX site ready for deployment"
46+
47+
- name: Upload artifact
48+
uses: actions/upload-pages-artifact@v3
49+
with:
50+
path: ./
51+
52+
# Deployment job
53+
deploy:
54+
environment:
55+
name: github-pages
56+
url: ${{ steps.deployment.outputs.page_url }}
57+
runs-on: ubuntu-latest
58+
needs: build
59+
if: github.ref == 'refs/heads/main' || github.ref == 'refs/heads/master'
60+
steps:
61+
- name: Deploy to GitHub Pages
62+
id: deployment
63+
uses: actions/deploy-pages@v4

0 commit comments

Comments
 (0)