@sdeverywhere/plugin-deploy 0.1.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/LICENSE +21 -0
- package/README.md +215 -0
- package/dist/index.cjs +506 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +77 -0
- package/dist/index.d.ts +77 -0
- package/dist/index.js +480 -0
- package/dist/index.js.map +1 -0
- package/package.json +54 -0
package/dist/index.d.cts
ADDED
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import { Plugin } from '@sdeverywhere/build';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Describes a build product that will be copied to the deployment directory.
|
|
5
|
+
*/
|
|
6
|
+
interface BuildProduct {
|
|
7
|
+
/**
|
|
8
|
+
* The name of the build product as used in the link in the top-level index page. If undefined,
|
|
9
|
+
* no link will be included in the top-level index page.
|
|
10
|
+
*/
|
|
11
|
+
displayName?: string;
|
|
12
|
+
/** The source path of the build product, relative to the project root directory (`rootDir`). */
|
|
13
|
+
srcPath: string;
|
|
14
|
+
/** The destination path of the build product, relative to the deployment directory (`deployDir`). */
|
|
15
|
+
dstPath: string;
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* The options that control the deployment process.
|
|
19
|
+
*/
|
|
20
|
+
interface DeployPluginOptions {
|
|
21
|
+
/**
|
|
22
|
+
* The base URL for the published project. This is used for determining the URLs
|
|
23
|
+
* for remote bundle files used by model-check and for other purposes.
|
|
24
|
+
*
|
|
25
|
+
* By default, the plugin assumes deployment to GitHub Pages, so if this property is
|
|
26
|
+
* undefined, the following default template will be used:
|
|
27
|
+
* ```
|
|
28
|
+
* baseUrl: 'https://{GH_USERNAME_OR_ORG}.github.io/{GH_REPO_NAME}'
|
|
29
|
+
* ```
|
|
30
|
+
*
|
|
31
|
+
* For example, if your GitHub username is "sdmodeler123", and your GitHub repository
|
|
32
|
+
* is called "my-sd-model", `baseUrl` will be set as follows:
|
|
33
|
+
* ```
|
|
34
|
+
* baseUrl: 'https://sdmodeler123.github.io/my-sd-model'
|
|
35
|
+
* ```
|
|
36
|
+
*
|
|
37
|
+
* IMPORTANT: If you set up GitHub Pages to use a custom domain, be sure to update
|
|
38
|
+
* this variable to use that custom domain, otherwise model-check may fail to load
|
|
39
|
+
* bundles due to cross origin redirect issues, for example:
|
|
40
|
+
* ```
|
|
41
|
+
* baseUrl: 'https://sdmodeler123.com/my-sd-model'
|
|
42
|
+
* ```
|
|
43
|
+
*
|
|
44
|
+
* If you use a different host/server (AWS, GitLab, etc) or publish to a different
|
|
45
|
+
* directory structure, you can update this variable to suit your needs, for example:
|
|
46
|
+
* ```
|
|
47
|
+
* baseUrl: 'https://sdmodeler123.com/projects/my-model'
|
|
48
|
+
* ```
|
|
49
|
+
*/
|
|
50
|
+
baseUrl?: string;
|
|
51
|
+
/**
|
|
52
|
+
* The path of the directory to which the build products will be copied.
|
|
53
|
+
* If undefined, defaults to the "deploy" directory under the configured
|
|
54
|
+
* project `prepDir`. The plugin will create this directory if it does not
|
|
55
|
+
* exist.
|
|
56
|
+
*
|
|
57
|
+
* This directory is ephemeral and should be listed in your `.gitignore` file.
|
|
58
|
+
*/
|
|
59
|
+
deployDir?: string;
|
|
60
|
+
/**
|
|
61
|
+
* The build products that will be copied to the `deployDir` directory.
|
|
62
|
+
* If undefined, defaults to the following build products that are typically
|
|
63
|
+
* generated by the build process (if any of these are not available, the
|
|
64
|
+
* plugin will skip copying them):
|
|
65
|
+
* - The app: `packages/app/public -> ${deployDir}/app`
|
|
66
|
+
* - The model-check bundle: `sde-prep/check-bundle.js -> ${deployDir}/extras/check-bundle.js`
|
|
67
|
+
* - The model-check report: `sde-prep/check-report -> ${deployDir}/extras/check-compare-to-base`
|
|
68
|
+
*
|
|
69
|
+
* The keys of the record should be a short name for the build product, used in the `index.json`
|
|
70
|
+
* file that is generated by the plugin.
|
|
71
|
+
*/
|
|
72
|
+
products?: Record<string, BuildProduct>;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
declare function deployPlugin(options?: DeployPluginOptions): Plugin;
|
|
76
|
+
|
|
77
|
+
export { type BuildProduct, type DeployPluginOptions, deployPlugin };
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import { Plugin } from '@sdeverywhere/build';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Describes a build product that will be copied to the deployment directory.
|
|
5
|
+
*/
|
|
6
|
+
interface BuildProduct {
|
|
7
|
+
/**
|
|
8
|
+
* The name of the build product as used in the link in the top-level index page. If undefined,
|
|
9
|
+
* no link will be included in the top-level index page.
|
|
10
|
+
*/
|
|
11
|
+
displayName?: string;
|
|
12
|
+
/** The source path of the build product, relative to the project root directory (`rootDir`). */
|
|
13
|
+
srcPath: string;
|
|
14
|
+
/** The destination path of the build product, relative to the deployment directory (`deployDir`). */
|
|
15
|
+
dstPath: string;
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* The options that control the deployment process.
|
|
19
|
+
*/
|
|
20
|
+
interface DeployPluginOptions {
|
|
21
|
+
/**
|
|
22
|
+
* The base URL for the published project. This is used for determining the URLs
|
|
23
|
+
* for remote bundle files used by model-check and for other purposes.
|
|
24
|
+
*
|
|
25
|
+
* By default, the plugin assumes deployment to GitHub Pages, so if this property is
|
|
26
|
+
* undefined, the following default template will be used:
|
|
27
|
+
* ```
|
|
28
|
+
* baseUrl: 'https://{GH_USERNAME_OR_ORG}.github.io/{GH_REPO_NAME}'
|
|
29
|
+
* ```
|
|
30
|
+
*
|
|
31
|
+
* For example, if your GitHub username is "sdmodeler123", and your GitHub repository
|
|
32
|
+
* is called "my-sd-model", `baseUrl` will be set as follows:
|
|
33
|
+
* ```
|
|
34
|
+
* baseUrl: 'https://sdmodeler123.github.io/my-sd-model'
|
|
35
|
+
* ```
|
|
36
|
+
*
|
|
37
|
+
* IMPORTANT: If you set up GitHub Pages to use a custom domain, be sure to update
|
|
38
|
+
* this variable to use that custom domain, otherwise model-check may fail to load
|
|
39
|
+
* bundles due to cross origin redirect issues, for example:
|
|
40
|
+
* ```
|
|
41
|
+
* baseUrl: 'https://sdmodeler123.com/my-sd-model'
|
|
42
|
+
* ```
|
|
43
|
+
*
|
|
44
|
+
* If you use a different host/server (AWS, GitLab, etc) or publish to a different
|
|
45
|
+
* directory structure, you can update this variable to suit your needs, for example:
|
|
46
|
+
* ```
|
|
47
|
+
* baseUrl: 'https://sdmodeler123.com/projects/my-model'
|
|
48
|
+
* ```
|
|
49
|
+
*/
|
|
50
|
+
baseUrl?: string;
|
|
51
|
+
/**
|
|
52
|
+
* The path of the directory to which the build products will be copied.
|
|
53
|
+
* If undefined, defaults to the "deploy" directory under the configured
|
|
54
|
+
* project `prepDir`. The plugin will create this directory if it does not
|
|
55
|
+
* exist.
|
|
56
|
+
*
|
|
57
|
+
* This directory is ephemeral and should be listed in your `.gitignore` file.
|
|
58
|
+
*/
|
|
59
|
+
deployDir?: string;
|
|
60
|
+
/**
|
|
61
|
+
* The build products that will be copied to the `deployDir` directory.
|
|
62
|
+
* If undefined, defaults to the following build products that are typically
|
|
63
|
+
* generated by the build process (if any of these are not available, the
|
|
64
|
+
* plugin will skip copying them):
|
|
65
|
+
* - The app: `packages/app/public -> ${deployDir}/app`
|
|
66
|
+
* - The model-check bundle: `sde-prep/check-bundle.js -> ${deployDir}/extras/check-bundle.js`
|
|
67
|
+
* - The model-check report: `sde-prep/check-report -> ${deployDir}/extras/check-compare-to-base`
|
|
68
|
+
*
|
|
69
|
+
* The keys of the record should be a short name for the build product, used in the `index.json`
|
|
70
|
+
* file that is generated by the plugin.
|
|
71
|
+
*/
|
|
72
|
+
products?: Record<string, BuildProduct>;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
declare function deployPlugin(options?: DeployPluginOptions): Plugin;
|
|
76
|
+
|
|
77
|
+
export { type BuildProduct, type DeployPluginOptions, deployPlugin };
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,480 @@
|
|
|
1
|
+
// src/plugin.ts
|
|
2
|
+
import { existsSync as existsSync3, mkdirSync as mkdirSync3, rmSync as rmSync2 } from "fs";
|
|
3
|
+
import { isAbsolute as isAbsolute2, join as joinPath3 } from "path";
|
|
4
|
+
|
|
5
|
+
// src/copy-products.ts
|
|
6
|
+
import { cpSync, existsSync, mkdirSync } from "fs";
|
|
7
|
+
import { isAbsolute, join as joinPath } from "path";
|
|
8
|
+
function copyProducts(context, options) {
|
|
9
|
+
function copyToDeployDir(src, dst) {
|
|
10
|
+
let fullSrcPath;
|
|
11
|
+
if (isAbsolute(src)) {
|
|
12
|
+
fullSrcPath = src;
|
|
13
|
+
} else {
|
|
14
|
+
fullSrcPath = joinPath(context.config.rootDir, src);
|
|
15
|
+
}
|
|
16
|
+
const skipIfNotExists = options.defaultProducts;
|
|
17
|
+
if (skipIfNotExists && !existsSync(fullSrcPath)) {
|
|
18
|
+
return;
|
|
19
|
+
}
|
|
20
|
+
context.log("verbose", `Copying '${src}' to '${dst}'...`);
|
|
21
|
+
const fullDstPath = joinPath(options.deployDir, dst);
|
|
22
|
+
if (existsSync(fullDstPath)) {
|
|
23
|
+
mkdirSync(fullDstPath, { recursive: true });
|
|
24
|
+
}
|
|
25
|
+
cpSync(fullSrcPath, fullDstPath, { recursive: true });
|
|
26
|
+
}
|
|
27
|
+
for (const product of Object.values(options.products)) {
|
|
28
|
+
copyToDeployDir(product.srcPath, product.dstPath);
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
// src/store-artifacts.ts
|
|
33
|
+
import { execSync } from "child_process";
|
|
34
|
+
import { existsSync as existsSync2, mkdirSync as mkdirSync2, cpSync as cpSync2, readFileSync, rmSync, writeFileSync } from "fs";
|
|
35
|
+
import { join as joinPath2 } from "path";
|
|
36
|
+
var artifactsBranchName = "artifacts";
|
|
37
|
+
var artifactsDir = "artifacts";
|
|
38
|
+
function storeArtifacts(context, options, currentBranchName) {
|
|
39
|
+
const deployDir = options.deployDir;
|
|
40
|
+
if (!existsSync2(deployDir)) {
|
|
41
|
+
context.log("error", `ERROR: Deployment directory '${deployDir}' does not exist`);
|
|
42
|
+
return false;
|
|
43
|
+
}
|
|
44
|
+
try {
|
|
45
|
+
if (!checkGitHubPagesConfiguration(context)) {
|
|
46
|
+
return false;
|
|
47
|
+
}
|
|
48
|
+
context.log("info", `Storing artifacts for branch '${currentBranchName}'...`);
|
|
49
|
+
const githubActor = process.env.GITHUB_ACTOR;
|
|
50
|
+
if (!githubActor) {
|
|
51
|
+
context.log(
|
|
52
|
+
"error",
|
|
53
|
+
"Failed to get the GitHub username associated with the latest push (GITHUB_ACTOR is not defined); the artifacts branch will not be updated"
|
|
54
|
+
);
|
|
55
|
+
return false;
|
|
56
|
+
}
|
|
57
|
+
execSync(`git config user.name "${githubActor}"`, { stdio: "inherit" });
|
|
58
|
+
execSync(`git config user.email "${githubActor}@users.noreply.github.com"`, { stdio: "inherit" });
|
|
59
|
+
let artifactsExists;
|
|
60
|
+
try {
|
|
61
|
+
const branchRef = `refs/remotes/origin/${artifactsBranchName}`;
|
|
62
|
+
execSync(`git show-ref --verify --quiet ${branchRef}`, { stdio: "ignore" });
|
|
63
|
+
artifactsExists = true;
|
|
64
|
+
} catch (_) {
|
|
65
|
+
artifactsExists = false;
|
|
66
|
+
}
|
|
67
|
+
if (!artifactsExists) {
|
|
68
|
+
context.log("verbose", `Creating orphan '${artifactsBranchName}' branch...`);
|
|
69
|
+
execSync(`git checkout --orphan ${artifactsBranchName}`, { stdio: "inherit" });
|
|
70
|
+
execSync("git rm -rf --cached .", { stdio: "inherit" });
|
|
71
|
+
const ignoredFiles = ["*", "!artifacts", "!artifacts/**", "!.gitignore"];
|
|
72
|
+
writeFileSync(".gitignore", ignoredFiles.join("\n"));
|
|
73
|
+
execSync("git add .gitignore", { stdio: "inherit" });
|
|
74
|
+
context.log("verbose", `Created .gitignore file for '${artifactsBranchName}' branch`);
|
|
75
|
+
} else {
|
|
76
|
+
context.log("verbose", `Switching to existing '${artifactsBranchName}' branch...`);
|
|
77
|
+
execSync(`git checkout ${artifactsBranchName}`, { stdio: "inherit" });
|
|
78
|
+
}
|
|
79
|
+
const currentBranchDir = joinPath2(artifactsDir, "branch", currentBranchName);
|
|
80
|
+
if (existsSync2(currentBranchDir)) {
|
|
81
|
+
context.log("verbose", `Removing existing branch directory '${currentBranchDir}'...`);
|
|
82
|
+
rmSync(currentBranchDir, { recursive: true });
|
|
83
|
+
}
|
|
84
|
+
context.log("verbose", `Copying staged files from '${deployDir}' to '${currentBranchDir}'...`);
|
|
85
|
+
cpSync2(deployDir, currentBranchDir, { recursive: true });
|
|
86
|
+
const currentBranchUrlPath = `branch/${currentBranchName}`;
|
|
87
|
+
const productSpecs = {};
|
|
88
|
+
for (const [name, product] of Object.entries(options.products || {})) {
|
|
89
|
+
productSpecs[name] = {
|
|
90
|
+
displayName: product.displayName,
|
|
91
|
+
path: `${currentBranchUrlPath}/${product.dstPath}`
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
const currentBranchSpec = {
|
|
95
|
+
name: currentBranchName,
|
|
96
|
+
path: currentBranchUrlPath,
|
|
97
|
+
lastModified: (/* @__PURE__ */ new Date()).toISOString(),
|
|
98
|
+
products: productSpecs
|
|
99
|
+
};
|
|
100
|
+
updateMetadata(context, options.baseUrl, currentBranchSpec);
|
|
101
|
+
if (currentBranchName === "main") {
|
|
102
|
+
const stagedAppSrcDir = joinPath2(deployDir, "app");
|
|
103
|
+
if (existsSync2(stagedAppSrcDir)) {
|
|
104
|
+
context.log("verbose", `Copying main branch app files to 'latest' directory...`);
|
|
105
|
+
if (existsSync2("latest")) {
|
|
106
|
+
context.log("verbose", `Removing existing 'latest' directory...`);
|
|
107
|
+
rmSync("latest", { recursive: true });
|
|
108
|
+
}
|
|
109
|
+
const stagedAppDstDir = joinPath2(artifactsDir, "latest");
|
|
110
|
+
cpSync2(stagedAppSrcDir, stagedAppDstDir, { recursive: true });
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
try {
|
|
114
|
+
const commitCount = parseInt(execSync("git rev-list --count HEAD", { encoding: "utf8" }).trim());
|
|
115
|
+
if (commitCount > 5) {
|
|
116
|
+
context.log(
|
|
117
|
+
"verbose",
|
|
118
|
+
`Found ${commitCount} commits, squashing older commits to reduce size of '${artifactsBranchName}' branch...`
|
|
119
|
+
);
|
|
120
|
+
const fifthCommitHash = execSync("git rev-list --skip=4 --max-count=1 HEAD", { encoding: "utf8" }).trim();
|
|
121
|
+
execSync(`git reset --soft ${fifthCommitHash}`, { stdio: "inherit" });
|
|
122
|
+
execSync("git commit --amend --no-edit", { stdio: "inherit" });
|
|
123
|
+
context.log("verbose", "Successfully squashed older commits");
|
|
124
|
+
}
|
|
125
|
+
} catch (_) {
|
|
126
|
+
context.log("verbose", "No commits found or error checking commit history, continuing...");
|
|
127
|
+
}
|
|
128
|
+
execSync(`git add ${artifactsDir}`, { stdio: "inherit" });
|
|
129
|
+
try {
|
|
130
|
+
execSync("git diff --cached --quiet", { stdio: "ignore" });
|
|
131
|
+
context.log("verbose", "No changes to commit");
|
|
132
|
+
} catch (_) {
|
|
133
|
+
const commitMessage = `build: update artifacts for branch ${currentBranchName}`;
|
|
134
|
+
execSync(`git commit -m "${commitMessage}"`, { stdio: "inherit" });
|
|
135
|
+
context.log("verbose", `Committed artifacts for branch '${currentBranchName}'...`);
|
|
136
|
+
}
|
|
137
|
+
context.log("info", `Pushing '${artifactsBranchName}' branch to remote...`);
|
|
138
|
+
execSync(`git push --force origin ${artifactsBranchName}`, { stdio: "inherit" });
|
|
139
|
+
context.log("info", `\u2705 Successfully stored artifacts for branch '${currentBranchName}'`);
|
|
140
|
+
return true;
|
|
141
|
+
} catch (error) {
|
|
142
|
+
context.log("error", "\u274C Error storing artifacts:");
|
|
143
|
+
console.error(error);
|
|
144
|
+
return false;
|
|
145
|
+
} finally {
|
|
146
|
+
context.log("verbose", `Switching to original '${currentBranchName}' branch...`);
|
|
147
|
+
execSync(`git checkout ${currentBranchName}`, { stdio: "inherit" });
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
function checkGitHubPagesConfiguration(context) {
|
|
151
|
+
context.log("verbose", "Checking if GitHub Pages is configured for this repo...");
|
|
152
|
+
const ownerAndRepo = process.env.GITHUB_REPOSITORY;
|
|
153
|
+
const ghAccept = '"Accept: application/vnd.github+json"';
|
|
154
|
+
const ghApiVersion = '"X-GitHub-Api-Version: 2022-11-28"';
|
|
155
|
+
const ghPagesApiPath = `/repos/${ownerAndRepo}/pages`;
|
|
156
|
+
let isGitHubPagesSetup;
|
|
157
|
+
try {
|
|
158
|
+
const pagesResponse = execSync(`gh api -H ${ghAccept} -H ${ghApiVersion} ${ghPagesApiPath}`);
|
|
159
|
+
const pagesMetadata = JSON.parse(pagesResponse.toString());
|
|
160
|
+
isGitHubPagesSetup = pagesMetadata.build_type === "workflow";
|
|
161
|
+
if (!isGitHubPagesSetup) {
|
|
162
|
+
context.log(
|
|
163
|
+
"verbose",
|
|
164
|
+
"GitHub Pages is not configured to use workflow builds for this repo, but found existing configuration:"
|
|
165
|
+
);
|
|
166
|
+
context.log("verbose", " build_type: " + pagesMetadata.build_type);
|
|
167
|
+
context.log("verbose", " source.branch: " + pagesMetadata.source?.branch);
|
|
168
|
+
context.log("verbose", " source.path: " + pagesMetadata.source?.path);
|
|
169
|
+
}
|
|
170
|
+
} catch (_) {
|
|
171
|
+
context.log("verbose", "No existing GitHub Pages configuration found");
|
|
172
|
+
isGitHubPagesSetup = false;
|
|
173
|
+
}
|
|
174
|
+
if (isGitHubPagesSetup) {
|
|
175
|
+
context.log("verbose", "GitHub Pages is already configured for this repo");
|
|
176
|
+
return true;
|
|
177
|
+
} else {
|
|
178
|
+
const msg = [];
|
|
179
|
+
msg.push("GitHub Pages is not configured for this repo");
|
|
180
|
+
msg.push("For now, you must manually enable GitHub Pages as follows:");
|
|
181
|
+
msg.push(" 1. Go to the GitHub repository settings");
|
|
182
|
+
msg.push(' 2. In the sidebar, select "Pages"');
|
|
183
|
+
msg.push(' 3. Under "Build and deployment", change "Source" to "GitHub Actions"');
|
|
184
|
+
msg.push(' 4. In the tab bar, select "Actions"');
|
|
185
|
+
msg.push(" 5. Click on the most recent failed workflow run");
|
|
186
|
+
msg.push(' 6. In the upper right corner, click "Re-run jobs" then "Re-run all jobs"');
|
|
187
|
+
context.log("error", msg.join("\n"));
|
|
188
|
+
return false;
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
function updateMetadata(context, baseUrl, currentBranchSpec) {
|
|
192
|
+
const metadataDir = joinPath2(artifactsDir, "metadata");
|
|
193
|
+
const indexJsonFile = joinPath2(metadataDir, "index.json");
|
|
194
|
+
const bundlesJsonFile = joinPath2(metadataDir, "bundles.json");
|
|
195
|
+
mkdirSync2(metadataDir, { recursive: true });
|
|
196
|
+
let branchSpecs = [];
|
|
197
|
+
if (existsSync2(indexJsonFile)) {
|
|
198
|
+
try {
|
|
199
|
+
branchSpecs = JSON.parse(readFileSync(indexJsonFile, "utf8"));
|
|
200
|
+
} catch (_) {
|
|
201
|
+
context.log("info", "\u26A0\uFE0F Could not parse existing index.json file, starting fresh");
|
|
202
|
+
branchSpecs = [];
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
branchSpecs = branchSpecs.filter((spec) => spec.name !== currentBranchSpec.name);
|
|
206
|
+
branchSpecs.push(currentBranchSpec);
|
|
207
|
+
branchSpecs.sort((a, b) => b.lastModified.localeCompare(a.lastModified));
|
|
208
|
+
writeFileSync(indexJsonFile, JSON.stringify(branchSpecs, null, 2));
|
|
209
|
+
const bundleSpecs = [];
|
|
210
|
+
for (const branchSpec of branchSpecs) {
|
|
211
|
+
if (branchSpec.products?.checkBundle) {
|
|
212
|
+
bundleSpecs.push({
|
|
213
|
+
name: branchSpec.name,
|
|
214
|
+
// TODO: For now we store the full URL to the bundle in the `bundles.json` file using
|
|
215
|
+
// the `baseUrl` value from the plugin options. Ideally we could use a relative URL
|
|
216
|
+
// here instead of encoding the full URL in the `bundles.json` file, but
|
|
217
|
+
// `@sdeverywhere/plugin-check` currently doesn't understand relative URLs.
|
|
218
|
+
url: `${baseUrl}/${branchSpec.products.checkBundle.path}`,
|
|
219
|
+
lastModified: branchSpec.lastModified
|
|
220
|
+
});
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
writeFileSync(bundlesJsonFile, JSON.stringify(bundleSpecs, null, 2));
|
|
224
|
+
context.log("verbose", `Updated metadata for branch '${currentBranchSpec.name}'`);
|
|
225
|
+
generateIndexHtml(context, branchSpecs);
|
|
226
|
+
}
|
|
227
|
+
function generateIndexHtml(context, branchSpecs) {
|
|
228
|
+
const indexHtmlPath = joinPath2(artifactsDir, "index.html");
|
|
229
|
+
const latestBuildDate = branchSpecs.find((branch) => branch.name === "main")?.lastModified || "";
|
|
230
|
+
function getBranchLinksHtml(branch) {
|
|
231
|
+
const links = [];
|
|
232
|
+
for (const product of Object.values(branch.products || {})) {
|
|
233
|
+
if (product.displayName) {
|
|
234
|
+
links.push(`<a href="${product.path}">${product.displayName}</a>`);
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
return links.join('<span class="separator">|</span>');
|
|
238
|
+
}
|
|
239
|
+
const htmlContent = `<!DOCTYPE html>
|
|
240
|
+
<html lang="en">
|
|
241
|
+
<head>
|
|
242
|
+
<meta charset="UTF-8">
|
|
243
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
244
|
+
<title>Branch Builds</title>
|
|
245
|
+
<style>
|
|
246
|
+
body {
|
|
247
|
+
font-family: monospace;
|
|
248
|
+
margin: 0;
|
|
249
|
+
padding: 20px;
|
|
250
|
+
background-color: #f5f5f5;
|
|
251
|
+
color: #000;
|
|
252
|
+
}
|
|
253
|
+
a, a:visited {
|
|
254
|
+
color: #2563eb;
|
|
255
|
+
}
|
|
256
|
+
hr {
|
|
257
|
+
margin: 20px 0;
|
|
258
|
+
border: none;
|
|
259
|
+
border-top: 1px solid #ccc;
|
|
260
|
+
}
|
|
261
|
+
.container {
|
|
262
|
+
overflow: hidden;
|
|
263
|
+
}
|
|
264
|
+
.grid {
|
|
265
|
+
display: grid;
|
|
266
|
+
grid-template-columns: max-content 1fr;
|
|
267
|
+
gap: 0 20px;
|
|
268
|
+
}
|
|
269
|
+
.header {
|
|
270
|
+
padding: 8px 0;
|
|
271
|
+
font-weight: 600;
|
|
272
|
+
}
|
|
273
|
+
.row {
|
|
274
|
+
display: contents;
|
|
275
|
+
}
|
|
276
|
+
.cell {
|
|
277
|
+
display: flex;
|
|
278
|
+
flex-direction: column;
|
|
279
|
+
padding: 8px 0;
|
|
280
|
+
}
|
|
281
|
+
.links {
|
|
282
|
+
display: flex;
|
|
283
|
+
}
|
|
284
|
+
.separator {
|
|
285
|
+
margin: 0 4px;
|
|
286
|
+
color: #9ca3af;
|
|
287
|
+
}
|
|
288
|
+
</style>
|
|
289
|
+
<script>
|
|
290
|
+
function formatDate(isoString) {
|
|
291
|
+
if (isoString.length === 0) {
|
|
292
|
+
return 'n/a'
|
|
293
|
+
}
|
|
294
|
+
const date = new Date(isoString)
|
|
295
|
+
const dateString = date.toLocaleDateString(undefined, { day: 'numeric', month: 'numeric', year: 'numeric' })
|
|
296
|
+
const timeString = date.toLocaleTimeString(undefined, { hour12: false, hour: '2-digit', minute: '2-digit' })
|
|
297
|
+
return \`\${dateString} at \${timeString}\`
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
// Format all dates on page load so that the time is displayed in the user's local timezone
|
|
301
|
+
document.addEventListener('DOMContentLoaded', function() {
|
|
302
|
+
document.querySelectorAll('.date').forEach(span => {
|
|
303
|
+
const timestamp = span.getAttribute('data-timestamp')
|
|
304
|
+
if (timestamp) {
|
|
305
|
+
span.textContent = formatDate(timestamp)
|
|
306
|
+
}
|
|
307
|
+
})
|
|
308
|
+
})
|
|
309
|
+
</script>
|
|
310
|
+
</head>
|
|
311
|
+
<body>
|
|
312
|
+
<div class="container">
|
|
313
|
+
<a href="latest">Latest production app</a>
|
|
314
|
+
<br />
|
|
315
|
+
(last updated: <span class="date" data-timestamp="${latestBuildDate}"></span>)
|
|
316
|
+
<hr />
|
|
317
|
+
<div class="grid">
|
|
318
|
+
<div class="header">Branch</div>
|
|
319
|
+
<div class="header">Last Updated</div>
|
|
320
|
+
${branchSpecs.map(
|
|
321
|
+
(branch) => `
|
|
322
|
+
<div class="row">
|
|
323
|
+
<div class="cell">
|
|
324
|
+
<span class="branch-name">${branch.name}</span>
|
|
325
|
+
<div class="links">
|
|
326
|
+
${getBranchLinksHtml(branch)}
|
|
327
|
+
</div>
|
|
328
|
+
</div>
|
|
329
|
+
<div class="cell">
|
|
330
|
+
<span class="date" data-timestamp="${branch.lastModified}"></span>
|
|
331
|
+
</div>
|
|
332
|
+
</div>`
|
|
333
|
+
).join("")}
|
|
334
|
+
</div>
|
|
335
|
+
</div>
|
|
336
|
+
</body>
|
|
337
|
+
</html>`;
|
|
338
|
+
writeFileSync(indexHtmlPath, htmlContent);
|
|
339
|
+
context.log("verbose", `Generated top-level index.html`);
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
// src/validate-branch-name.ts
|
|
343
|
+
function validateBranchName(branchName) {
|
|
344
|
+
const validBranchPattern = /^[/\-_a-zA-Z0-9]+$/;
|
|
345
|
+
if (!validBranchPattern.test(branchName)) {
|
|
346
|
+
throw new Error(
|
|
347
|
+
`Branch name '${branchName}' contains invalid characters; branch names must only contain: /, -, _, a-z, A-Z, 0-9`
|
|
348
|
+
);
|
|
349
|
+
}
|
|
350
|
+
if (branchName.startsWith("/") || branchName.endsWith("/")) {
|
|
351
|
+
throw new Error(`Branch name '${branchName}' cannot start or end with "/"`);
|
|
352
|
+
}
|
|
353
|
+
if (branchName.includes("//")) {
|
|
354
|
+
throw new Error(`Branch name '${branchName}' cannot contain consecutive slashes "//"`);
|
|
355
|
+
}
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
// src/plugin.ts
|
|
359
|
+
function deployPlugin(options) {
|
|
360
|
+
return new DeployPlugin(options ?? {});
|
|
361
|
+
}
|
|
362
|
+
var DeployPlugin = class {
|
|
363
|
+
constructor(userOptions) {
|
|
364
|
+
this.userOptions = userOptions;
|
|
365
|
+
}
|
|
366
|
+
async init() {
|
|
367
|
+
const branchName = getCurrentBranchName();
|
|
368
|
+
if (branchName) {
|
|
369
|
+
validateBranchName(branchName);
|
|
370
|
+
}
|
|
371
|
+
}
|
|
372
|
+
async postBuild(context) {
|
|
373
|
+
if (context.config.mode !== "production") {
|
|
374
|
+
return true;
|
|
375
|
+
}
|
|
376
|
+
context.log("info", "\nPreparing to deploy build products...");
|
|
377
|
+
const resolvedOptions = resolveOptions(context, this.userOptions);
|
|
378
|
+
const deployDir = resolvedOptions.deployDir;
|
|
379
|
+
if (existsSync3(deployDir)) {
|
|
380
|
+
context.log("verbose", "Removing existing deploy directory...");
|
|
381
|
+
rmSync2(deployDir, { recursive: true, force: true });
|
|
382
|
+
}
|
|
383
|
+
context.log("verbose", "Creating deploy directory...");
|
|
384
|
+
mkdirSync3(deployDir, { recursive: true });
|
|
385
|
+
context.log("verbose", "Copying build products to deploy directory...");
|
|
386
|
+
copyProducts(context, resolvedOptions);
|
|
387
|
+
if (process.env.VITEST === "true") {
|
|
388
|
+
context.log("info", "Skipping `storeArtifacts` step in test mode...");
|
|
389
|
+
return true;
|
|
390
|
+
}
|
|
391
|
+
const currentBranchName = getCurrentBranchName();
|
|
392
|
+
if (!currentBranchName) {
|
|
393
|
+
context.log(
|
|
394
|
+
"info",
|
|
395
|
+
"Failed to get the name of the current branch (GITHUB_REF_NAME is not defined); artifacts branch will not be updated"
|
|
396
|
+
);
|
|
397
|
+
return true;
|
|
398
|
+
}
|
|
399
|
+
context.log("verbose", "Updating artifacts branch with build products...");
|
|
400
|
+
return storeArtifacts(context, resolvedOptions, currentBranchName);
|
|
401
|
+
}
|
|
402
|
+
};
|
|
403
|
+
function getRepoOwnerAndName() {
|
|
404
|
+
const ownerAndRepo = process.env.GITHUB_REPOSITORY;
|
|
405
|
+
if (ownerAndRepo) {
|
|
406
|
+
const [owner, repo] = ownerAndRepo.split("/");
|
|
407
|
+
return [owner, repo];
|
|
408
|
+
} else {
|
|
409
|
+
return void 0;
|
|
410
|
+
}
|
|
411
|
+
}
|
|
412
|
+
function getCurrentBranchName() {
|
|
413
|
+
if (process.env.VITEST) {
|
|
414
|
+
return process.env.TEST_BRANCH_NAME;
|
|
415
|
+
} else {
|
|
416
|
+
return process.env.GITHUB_REF_NAME;
|
|
417
|
+
}
|
|
418
|
+
}
|
|
419
|
+
function resolveOptions(context, userOptions) {
|
|
420
|
+
let baseUrl;
|
|
421
|
+
if (userOptions.baseUrl) {
|
|
422
|
+
baseUrl = userOptions.baseUrl;
|
|
423
|
+
} else {
|
|
424
|
+
const repoOwnerAndName = getRepoOwnerAndName();
|
|
425
|
+
if (repoOwnerAndName) {
|
|
426
|
+
const [owner, repo] = repoOwnerAndName;
|
|
427
|
+
baseUrl = `https://${owner}.github.io/${repo}`;
|
|
428
|
+
} else {
|
|
429
|
+
context.log(
|
|
430
|
+
"info",
|
|
431
|
+
"Failed to get the name of the repository (GITHUB_REPOSITORY is not defined); the deploy directory will be populated, but the artifacts branch will not be updated"
|
|
432
|
+
);
|
|
433
|
+
baseUrl = void 0;
|
|
434
|
+
}
|
|
435
|
+
}
|
|
436
|
+
let deployDir;
|
|
437
|
+
if (userOptions.deployDir) {
|
|
438
|
+
if (isAbsolute2(userOptions.deployDir)) {
|
|
439
|
+
deployDir = userOptions.deployDir;
|
|
440
|
+
} else {
|
|
441
|
+
deployDir = joinPath3(context.config.rootDir, userOptions.deployDir);
|
|
442
|
+
}
|
|
443
|
+
} else {
|
|
444
|
+
deployDir = joinPath3(context.config.prepDir, "deploy");
|
|
445
|
+
}
|
|
446
|
+
let products;
|
|
447
|
+
let defaultProducts;
|
|
448
|
+
if (userOptions.products) {
|
|
449
|
+
products = userOptions.products;
|
|
450
|
+
defaultProducts = false;
|
|
451
|
+
} else {
|
|
452
|
+
products = {
|
|
453
|
+
app: {
|
|
454
|
+
displayName: "app",
|
|
455
|
+
srcPath: "packages/app/public",
|
|
456
|
+
dstPath: "app"
|
|
457
|
+
},
|
|
458
|
+
checkReport: {
|
|
459
|
+
displayName: "checks",
|
|
460
|
+
srcPath: "sde-prep/check-report",
|
|
461
|
+
dstPath: "extras/check-compare-to-base"
|
|
462
|
+
},
|
|
463
|
+
checkBundle: {
|
|
464
|
+
srcPath: "sde-prep/check-bundle.js",
|
|
465
|
+
dstPath: "extras/check-bundle.js"
|
|
466
|
+
}
|
|
467
|
+
};
|
|
468
|
+
defaultProducts = true;
|
|
469
|
+
}
|
|
470
|
+
return {
|
|
471
|
+
baseUrl,
|
|
472
|
+
deployDir,
|
|
473
|
+
products,
|
|
474
|
+
defaultProducts
|
|
475
|
+
};
|
|
476
|
+
}
|
|
477
|
+
export {
|
|
478
|
+
deployPlugin
|
|
479
|
+
};
|
|
480
|
+
//# sourceMappingURL=index.js.map
|