@micazoyolli/foundation 0.4.0 → 0.4.1

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.
@@ -8,7 +8,7 @@ const main = async () => {
8
8
  console.log('Repository left unchanged.');
9
9
  return;
10
10
  }
11
- console.log(`Published ${result.commit} to ${result.remote}/${result.deploymentBranch}.`);
11
+ console.log(`${result.initialDeployment ? 'Created' : 'Published'} ${result.commit} to ${result.remote}/${result.deploymentBranch}.`);
12
12
  console.log('Repository left clean on main.');
13
13
  };
14
14
  main().catch((error) => {
@@ -9,6 +9,7 @@ export type GithubPagesDeployOptions = {
9
9
  export type GithubPagesDeployResult = {
10
10
  commit?: string;
11
11
  deploymentBranch: string;
12
+ initialDeployment: boolean;
12
13
  pushed: boolean;
13
14
  reason?: string;
14
15
  remote: string;
@@ -1,6 +1,6 @@
1
1
  import { cp, mkdtemp, readdir, rm, stat } from 'node:fs/promises';
2
2
  import { tmpdir } from 'node:os';
3
- import { basename, join, resolve } from 'node:path';
3
+ import { basename, isAbsolute, join, relative, resolve, sep } from 'node:path';
4
4
  import { spawnSync } from 'node:child_process';
5
5
  const DEFAULT_BUILD_DIR = 'dist';
6
6
  const DEFAULT_DEPLOYMENT_BRANCH = 'gh-pages';
@@ -51,6 +51,27 @@ const getRemoteTrackingRef = (remote, branch) => `refs/remotes/${remote}/${branc
51
51
  const fetchRemoteBranch = (repositoryRoot, remote, branch) => {
52
52
  runGit(['fetch', remote, `+${branch}:${getRemoteTrackingRef(remote, branch)}`], repositoryRoot);
53
53
  };
54
+ const remoteBranchExists = (repositoryRoot, remote, branch) => {
55
+ const result = spawnSync('git', ['ls-remote', '--exit-code', '--heads', remote, branch], {
56
+ cwd: repositoryRoot,
57
+ encoding: 'utf8',
58
+ env: {
59
+ ...process.env,
60
+ GIT_TERMINAL_PROMPT: '0',
61
+ },
62
+ });
63
+ if (result.error) {
64
+ throw new GithubPagesDeployError(`Failed to check ${remote}/${branch}: ${result.error.message}`);
65
+ }
66
+ if (result.status === 0) {
67
+ return true;
68
+ }
69
+ if (result.status === 2) {
70
+ return false;
71
+ }
72
+ const details = (result.stderr || result.stdout || '').trim();
73
+ throw new GithubPagesDeployError(`Git command failed: git ls-remote --exit-code --heads ${remote} ${branch}${details ? `\n${details}` : ''}`);
74
+ };
54
75
  const getRepositoryRoot = (cwd) => {
55
76
  try {
56
77
  return runGit(['rev-parse', '--show-toplevel'], cwd).stdout;
@@ -59,33 +80,42 @@ const getRepositoryRoot = (cwd) => {
59
80
  throw new GithubPagesDeployError('Current directory must be inside a Git repository.');
60
81
  }
61
82
  };
83
+ const toGitPath = (path) => path.split(sep).join('/');
84
+ const getBuildDirectoryPathspec = (repositoryRoot, buildDirectory) => {
85
+ const relativeBuildDirectory = relative(repositoryRoot, buildDirectory);
86
+ if (!relativeBuildDirectory || relativeBuildDirectory === '..' || relativeBuildDirectory.startsWith(`..${sep}`) || isAbsolute(relativeBuildDirectory)) {
87
+ throw new GithubPagesDeployError(`Build directory must be inside the repository: ${buildDirectory}`);
88
+ }
89
+ return toGitPath(relativeBuildDirectory);
90
+ };
91
+ const assertBuildDirectoryIsUntracked = (repositoryRoot, buildDirectoryPathspec, sourceBranch) => {
92
+ const trackedFiles = runGit(['ls-files', '--', buildDirectoryPathspec], repositoryRoot).stdout;
93
+ if (trackedFiles) {
94
+ throw new GithubPagesDeployError([
95
+ `Build directory "${buildDirectoryPathspec}" must not be tracked by Git.`,
96
+ `Remove it from ${sourceBranch} with: git rm -r --cached ${buildDirectoryPathspec}`,
97
+ `Keep "${buildDirectoryPathspec}/" ignored in .gitignore before deploying.`,
98
+ ].join('\n'));
99
+ }
100
+ };
62
101
  const assertCleanWorkingTree = (repositoryRoot) => {
63
102
  const status = runGit(['status', '--porcelain'], repositoryRoot).stdout;
64
103
  if (status) {
65
104
  throw new GithubPagesDeployError(`Working tree must be clean before deploying.\n${status}`);
66
105
  }
67
106
  };
68
- const assertSyncedBranch = (repositoryRoot, sourceBranch, remote, deploymentBranch) => {
107
+ const assertSyncedBranch = (repositoryRoot, sourceBranch, remote) => {
69
108
  const currentBranch = runGit(['branch', '--show-current'], repositoryRoot).stdout;
70
109
  if (currentBranch !== sourceBranch) {
71
110
  throw new GithubPagesDeployError(`Current branch must be "${sourceBranch}". Found "${currentBranch || 'detached HEAD'}".`);
72
111
  }
73
112
  fetchRemoteBranch(repositoryRoot, remote, sourceBranch);
74
- fetchRemoteBranch(repositoryRoot, remote, deploymentBranch);
75
113
  const localHead = runGit(['rev-parse', sourceBranch], repositoryRoot).stdout;
76
114
  const remoteHead = runGit(['rev-parse', `${remote}/${sourceBranch}`], repositoryRoot).stdout;
77
115
  if (localHead !== remoteHead) {
78
116
  throw new GithubPagesDeployError(`${sourceBranch} must be synchronized with ${remote}/${sourceBranch} before deploying.`);
79
117
  }
80
118
  };
81
- const assertDeploymentBranchExists = (repositoryRoot, deploymentRef) => {
82
- try {
83
- runGit(['rev-parse', '--verify', `${deploymentRef}^{commit}`], repositoryRoot);
84
- }
85
- catch {
86
- throw new GithubPagesDeployError(`Deployment branch "${deploymentRef}" must exist before deploying.`);
87
- }
88
- };
89
119
  const assertBuildDirectory = async (buildDirectory) => {
90
120
  let buildStat;
91
121
  try {
@@ -133,15 +163,23 @@ export const deployGithubPages = async (options = {}) => {
133
163
  assertSafeGitRefPart(resolved.deploymentBranch, 'deployment branch');
134
164
  const repositoryRoot = getRepositoryRoot(resolved.cwd);
135
165
  const buildDirectory = resolve(repositoryRoot, resolved.buildDir);
166
+ const buildDirectoryPathspec = getBuildDirectoryPathspec(repositoryRoot, buildDirectory);
136
167
  const deploymentRef = `${resolved.remote}/${resolved.deploymentBranch}`;
168
+ assertBuildDirectoryIsUntracked(repositoryRoot, buildDirectoryPathspec, resolved.sourceBranch);
137
169
  assertCleanWorkingTree(repositoryRoot);
138
- assertSyncedBranch(repositoryRoot, resolved.sourceBranch, resolved.remote, resolved.deploymentBranch);
139
- assertDeploymentBranchExists(repositoryRoot, deploymentRef);
170
+ assertSyncedBranch(repositoryRoot, resolved.sourceBranch, resolved.remote);
171
+ const deploymentBranchExists = remoteBranchExists(repositoryRoot, resolved.remote, resolved.deploymentBranch);
172
+ if (deploymentBranchExists) {
173
+ fetchRemoteBranch(repositoryRoot, resolved.remote, resolved.deploymentBranch);
174
+ }
140
175
  await assertBuildDirectory(buildDirectory);
141
176
  const tempRoot = await mkdtemp(join(tmpdir(), 'micazoyolli-gh-pages-'));
142
177
  const worktreePath = join(tempRoot, basename(repositoryRoot));
143
178
  try {
144
- runGit(['worktree', 'add', '--detach', worktreePath, deploymentRef], repositoryRoot);
179
+ runGit(['worktree', 'add', '--detach', worktreePath, deploymentBranchExists ? deploymentRef : resolved.sourceBranch], repositoryRoot);
180
+ if (!deploymentBranchExists) {
181
+ runGit(['checkout', '--orphan', resolved.deploymentBranch], worktreePath);
182
+ }
145
183
  await clearWorktree(worktreePath);
146
184
  await cp(buildDirectory, worktreePath, {
147
185
  force: true,
@@ -151,6 +189,7 @@ export const deployGithubPages = async (options = {}) => {
151
189
  if (!deploymentStatus) {
152
190
  return {
153
191
  deploymentBranch: resolved.deploymentBranch,
192
+ initialDeployment: false,
154
193
  pushed: false,
155
194
  reason: 'Deployment content is unchanged.',
156
195
  remote: resolved.remote,
@@ -165,6 +204,7 @@ export const deployGithubPages = async (options = {}) => {
165
204
  return {
166
205
  commit,
167
206
  deploymentBranch: resolved.deploymentBranch,
207
+ initialDeployment: !deploymentBranchExists,
168
208
  pushed: true,
169
209
  remote: resolved.remote,
170
210
  repositoryRoot,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@micazoyolli/foundation",
3
- "version": "0.4.0",
3
+ "version": "0.4.1",
4
4
  "description": "Fundamentos frontend no visuales para el ecosistema técnico de Nad.",
5
5
  "license": "MIT",
6
6
  "type": "module",