64 Commits

Author SHA1 Message Date
James M. Greene
e22fa7ebed Merge pull request #11 from actions/improve-action-manifest
Improve Actions manifest
2022-08-05 13:40:43 -05:00
James M. Greene
635cafe472 Use single quotes everywhere for consistency in Actions manifest 2022-08-05 13:29:15 -05:00
James M. Greene
7c6340377c Add author to Action manifest 2022-08-05 13:28:40 -05:00
James M. Greene
542786ddbc Improve and shorten description for Marketplace publication 2022-08-05 13:27:44 -05:00
James M. Greene
c4a801b850 Merge pull request #9 from actions/dist-check-workflow
Add workflow to verify distributables are built
2022-08-05 13:10:05 -05:00
James M. Greene
fcc627b194 Merge branch 'main' into dist-check-workflow 2022-08-05 13:09:06 -05:00
James M. Greene
2fc7b604aa Add explicit permissions to workflow 2022-08-05 13:08:52 -05:00
James M. Greene
bcfa2c863c Merge pull request #8 from actions/cleanup-test-workflow
Cleanup formatting of test workflow
2022-08-05 13:07:40 -05:00
James M. Greene
c95cb0d322 Merge pull request #7 from actions/pkg-metadata
Correct a few metadata field values in the `package.json`
2022-08-05 13:07:25 -05:00
James M. Greene
d2f9056bbc Merge pull request #6 from actions/optional-enablement
Make Pages enablement (and write permissions) optional
2022-08-05 13:06:58 -05:00
James M. Greene
b619d6bb2a Make certain the env var isn't present during testing 2022-08-05 12:59:28 -05:00
James M. Greene
1f779755f8 Readd distributables 2022-08-05 12:13:48 -05:00
James M. Greene
bce63914b1 Incorporate the spirit of changes from https://github.com/actions/configure-pages/pull/10 2022-08-05 12:13:23 -05:00
James M. Greene
0a94d4c3bb Merge branch 'main' into optional-enablement 2022-08-05 12:05:17 -05:00
Mingzi
d868d5fe4e Merge pull request #10 from actions/fix-hard-coded-api-endpoint
Fix hardcoded api endpoint
2022-08-05 09:55:37 -07:00
yimysty
d8dd1327a1 fix hardcoded api endpoint 2022-08-05 09:52:17 -07:00
James M. Greene
2a2b0fadb6 Remove diff of 'pre' since this Action doesn't have that directory 2022-08-03 21:47:18 -05:00
James M. Greene
e2ea66d572 Revise comment 2022-08-03 21:46:57 -05:00
James M. Greene
55225928a1 Add workflow to verify distributables are built 2022-08-03 18:30:55 -05:00
James M. Greene
62a7d4f2dc Cleanup formatting of test workflow 2022-08-03 18:30:19 -05:00
James M. Greene
929a1c7425 Correct a few metadata field values in the package.json 2022-08-03 18:24:31 -05:00
James M. Greene
17536ca11a Update organization name in tests 2022-08-03 18:19:57 -05:00
James M. Greene
d801b818b5 Update dist 2022-08-03 18:10:24 -05:00
James M. Greene
fc0fb71264 Delete old modules 2022-08-03 18:10:04 -05:00
James M. Greene
af945d6133 Update index.js to utilize new modules 2022-08-03 18:09:46 -05:00
James M. Greene
b2561d383d Add dedicated Actions-outputs module 2022-08-03 18:09:04 -05:00
James M. Greene
bb42d7b1cb Add new API client to consolidate REST API calls 2022-08-03 18:08:29 -05:00
James M. Greene
0455a16aca Allow a user to manually opt-out of Pages site enablement 2022-08-02 20:45:01 -05:00
Yoann Chaudet
677bce1797 Merge pull request #3 from actions/paper-spa-updates
Pull in latest updates
2022-07-26 11:49:12 -07:00
James M. Greene
49843a9170 Merge pull request #16 from paper-spa/latest-ecma
Update Espree configuration to allow for latest supported ECMA version
2022-07-22 18:06:07 -05:00
James M. Greene
86288138eb Fix spacing 2022-07-22 18:04:52 -05:00
James M. Greene
98aa330a29 Update Espree configuration to allow for latest supported ECMA version 2022-07-22 18:03:26 -05:00
James M. Greene
e993237306 Update dist 2022-07-22 18:02:57 -05:00
James M. Greene
1fb9ba6653 Add tests that will fail with ECMA 6 parsing 2022-07-22 18:01:42 -05:00
James M. Greene
1457a09e2b Revert to using ECMA 6 2022-07-22 18:01:23 -05:00
James M. Greene
3480141499 Update Espree configuration to allow for latest supported ECMA version 2022-07-22 14:10:25 -05:00
Yoann Chaudet
8981dbcb59 Merge pull request #15 from paper-spa/server-static 2022-07-21 08:25:59 -07:00
Yoann Chaudet
7875e4af8a target not server 2022-07-20 15:28:26 -07:00
Yoann Chaudet
844ceaff5a Do a bit more configuration for nuxt, set server = 'static' 2022-07-20 13:14:25 -07:00
Yoann Chaudet
398adf731a Merge pull request #14 from paper-spa/fallback
Rewrite the AST logic to be more complete
2022-07-20 12:53:08 -07:00
Yoann Chaudet
dddd39c6b9 Documentation 2022-07-19 17:16:40 -07:00
Yoann Chaudet
084dc5123d experimental 2022-07-19 17:15:09 -07:00
Yoann Chaudet
3d51fe705a false true 2022-07-19 17:05:37 -07:00
Yoann Chaudet
3d2f0e5994 Refactor to inject multiple properties 2022-07-19 17:03:44 -07:00
Yoann Chaudet
ad121920a0 Add blank fixtures for nuxt 2022-07-19 16:31:11 -07:00
Yoann Chaudet
53eec1a8cc fix next 2022-07-19 16:27:57 -07:00
Yoann Chaudet
69fea3d62e typo 2022-07-19 16:21:56 -07:00
Yoann Chaudet
05829a2e3d Update dist 2022-07-19 16:17:11 -07:00
Yoann Chaudet
33fad13833 Fix context: 2022-07-19 16:16:35 -07:00
Yoann Chaudet
931e155079 Add tests for config-parser 2022-07-19 16:15:25 -07:00
James M. Greene
250c4fb989 Update dist files 2022-07-19 16:54:17 -05:00
Yoann Chaudet
a774891038 update dist 2022-07-19 11:52:32 -07:00
Yoann Chaudet
5941f86157 Fix tests 2022-07-19 11:52:20 -07:00
Yoann Chaudet
8f29fe2748 Update dist 2022-07-19 11:50:09 -07:00
Yoann Chaudet
ba4576e776 Add set-pages-path tests 2022-07-19 11:48:11 -07:00
James M. Greene
43a5456c3f Fix dist after installing dependencies 2022-07-19 13:02:27 -05:00
James M. Greene
2d3e762f19 Update dist for testing 2022-07-19 12:59:37 -05:00
Yoann Chaudet
f830cbcb66 Cleanup the whole config-parser class 2022-07-18 19:13:56 -07:00
Yoann Chaudet
3efd613ed2 wip: add formatter support to the output file 2022-07-18 15:51:32 -07:00
Yoann Chaudet
cf1ab90e59 Merge pull request #13 from paper-spa/reformat-all-the-things
Add prettier configuration
2022-07-18 15:33:22 -07:00
Yoann Chaudet
e2bf7f26fb Reformat nextjs fixtures 2022-07-18 15:14:53 -07:00
Yoann Chaudet
df54bc901b Update distribution 2022-07-18 15:08:08 -07:00
Yoann Chaudet
61e591d3b5 Add prettier configuration 2022-07-18 15:06:14 -07:00
Yoann Chaudet
bfe36bc062 Merge pull request #2 from actions/release
Change release trigger
2022-07-13 15:36:34 -07:00
61 changed files with 1656 additions and 921 deletions

47
.github/workflows/check-dist.yml vendored Normal file
View File

@@ -0,0 +1,47 @@
# When you reference this Action with `uses:` in a workflow,
# `dist/index.js` is the code that will run.
# For our project, we generate this file using `ncc`.
# We need to make sure the checked-in `dist/index.js` actually matches what we expect it to be.
name: Check dist/
on:
push:
branches:
- main
paths-ignore:
- '**.md'
pull_request:
paths-ignore:
- '**.md'
workflow_dispatch:
permissions:
contents: read
jobs:
check-dist:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v3
- name: Setup Node.js
uses: actions/setup-node@v3
with:
node-version: 16.x
cache: 'npm'
- name: Install dependencies
run: npm ci
- name: Rebuild the dist/ directory
run: npm run prepare
- name: Compare the expected and actual dist/ directories
run: |
if [ "$(git diff --ignore-space-at-eol dist/ | wc -l)" -gt "0" ]; then
echo "Detected uncommitted changes after build in dist folder. See status below:"
git diff
exit 1
fi

View File

@@ -10,16 +10,17 @@ jobs:
test: test:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- name: Checkout - name: Checkout
uses: actions/checkout@v3 uses: actions/checkout@v3
- name: Set Node.JS - name: Setup Node.JS
uses: actions/setup-node@v3 uses: actions/setup-node@v3
with: with:
node-version: 16.x node-version: 16.x
cache: 'npm'
- name: Install dependencies - name: Install dependencies
run: npm ci run: npm ci
- name: Run tests - name: Run tests
run: npm test run: npm test

1
.gitignore vendored
View File

@@ -1,4 +1,5 @@
node_modules/ node_modules/
/src/fixtures/tmp
# Editors # Editors
.vscode/ .vscode/

10
.prettierrc.yaml Normal file
View File

@@ -0,0 +1,10 @@
# Prettier (formatter) configuration
---
printWidth: 80
tabWidth: 2
useTabs: false
semi: false
singleQuote: true
trailingComma: none
bracketSpacing: false
arrowParens: avoid

View File

@@ -2,6 +2,8 @@
An action to enable Pages and extract various metadata about a site. It can also be used to configure various static site generators we support as [starter workflows][starter-workflows]. An action to enable Pages and extract various metadata about a site. It can also be used to configure various static site generators we support as [starter workflows][starter-workflows].
See [`set-pages-path.js`](./src/set-pages-path.js) for more details on how we configure static site generators to work "out of the box" with GitHub Pages.
# Usage # Usage
See [action.yml](action.yml) and the [Pages starter workflows][starter-workflows]. See [action.yml](action.yml) and the [Pages starter workflows][starter-workflows].

View File

@@ -1,16 +1,21 @@
name: "Configure Pages" name: 'Configure Pages'
description: "An action to enable Pages and extract various metadata about a site. It can also be used to configure various static site generators we support as starter workflows." description: 'A GitHub Action to enable Pages, extract various metadata about a site, and configure some supported static site generators.'
author: 'GitHub'
runs: runs:
using: "node16" using: 'node16'
main: "dist/index.js" main: 'dist/index.js'
inputs: inputs:
static_site_generator: static_site_generator:
description: "Optional static site generator to attempt to configure (nuxt, next or gatsby)" description: 'Optional static site generator to attempt to configure (nuxt, next or gatsby)'
required: false required: false
token: token:
description: "GitHub token" description: 'GitHub token'
default: ${{ github.token }} default: ${{ github.token }}
required: true required: true
enablement:
description: 'Should a Pages site be enabled for the repository if not so already?'
default: 'true'
required: false
outputs: outputs:
base_url: base_url:
description: 'GitHub Pages site full base URL. Examples: "https://octocat.github.io/my-repo/", "https://octocat.github.io/", "https://www.example.com/"' description: 'GitHub Pages site full base URL. Examples: "https://octocat.github.io/my-repo/", "https://octocat.github.io/", "https://www.example.com/"'

2
dist/gatsby.js vendored Normal file
View File

@@ -0,0 +1,2 @@
// Default Pages configuration for Gatsby
module.exports = {}

797
dist/index.js vendored
View File

@@ -14000,107 +14000,6 @@ function plural(ms, msAbs, n, name) {
} }
/***/ }),
/***/ 3259:
/***/ (function(module) {
void function(global) {
'use strict';
// ValueError :: String -> Error
function ValueError(message) {
var err = new Error(message);
err.name = 'ValueError';
return err;
}
// defaultTo :: a,a? -> a
function defaultTo(x, y) {
return y == null ? x : y;
}
// create :: Object -> String,*... -> String
function create(transformers) {
return function(template) {
var args = Array.prototype.slice.call(arguments, 1);
var idx = 0;
var state = 'UNDEFINED';
return template.replace(
/([{}])\1|[{](.*?)(?:!(.+?))?[}]/g,
function(match, literal, _key, xf) {
if (literal != null) {
return literal;
}
var key = _key;
if (key.length > 0) {
if (state === 'IMPLICIT') {
throw ValueError('cannot switch from ' +
'implicit to explicit numbering');
}
state = 'EXPLICIT';
} else {
if (state === 'EXPLICIT') {
throw ValueError('cannot switch from ' +
'explicit to implicit numbering');
}
state = 'IMPLICIT';
key = String(idx);
idx += 1;
}
var value = defaultTo('', lookup(args, key.split('.')));
if (xf == null) {
return value;
} else if (Object.prototype.hasOwnProperty.call(transformers, xf)) {
return transformers[xf](value);
} else {
throw ValueError('no transformer named "' + xf + '"');
}
}
);
};
}
function lookup(_obj, _path) {
var obj = _obj;
var path = _path;
if (!/^\d+$/.test(path[0])) {
path = ['0'].concat(path);
}
for (var idx = 0; idx < path.length; idx += 1) {
var key = path[idx];
obj = typeof obj[key] === 'function' ? obj[key]() : obj[key];
}
return obj;
}
// format :: String,*... -> String
var format = create({});
// format.create :: Object -> String,*... -> String
format.create = create;
// format.extend :: Object,Object -> ()
format.extend = function(prototype, transformers) {
var $format = create(transformers);
prototype.format = function() {
var args = Array.prototype.slice.call(arguments);
args.unshift(this);
return $format.apply(global, args);
};
};
/* istanbul ignore else */
if (true) {
module.exports = format;
} else {}
}.call(this, this);
/***/ }), /***/ }),
/***/ 9318: /***/ 9318:
@@ -14524,6 +14423,104 @@ if (process.env.NODE_DEBUG && /\btunnel\b/.test(process.env.NODE_DEBUG)) {
exports.debug = debug; // for test exports.debug = debug; // for test
/***/ }),
/***/ 9432:
/***/ ((module, __unused_webpack_exports, __nccwpck_require__) => {
const axios = __nccwpck_require__(6545)
const core = __nccwpck_require__(2186)
function getApiBaseUrl() {
return process.env.GITHUB_API_URL || 'https://api.github.com'
}
async function enablePagesSite({ repositoryNwo, githubToken }) {
const pagesEndpoint = `${getApiBaseUrl()}/repos/${repositoryNwo}/pages`
try {
const response = await axios.post(
pagesEndpoint,
{ build_type: 'workflow' },
{
headers: {
Accept: 'application/vnd.github.v3+json',
Authorization: `Bearer ${githubToken}`,
'Content-type': 'application/json'
}
}
)
const pageObject = response.data
return pageObject
} catch (error) {
if (error.response && error.response.status === 409) {
return null
}
throw error
}
}
async function getPagesSite({ repositoryNwo, githubToken }) {
try {
const pagesEndpoint = `${getApiBaseUrl()}/repos/${repositoryNwo}/pages`
const response = await axios.get(pagesEndpoint, {
headers: {
Accept: 'application/vnd.github.v3+json',
Authorization: `Bearer ${githubToken}`
}
})
const pageObject = response.data
return pageObject
} catch (error) {
throw error
}
}
async function findOrCreatePagesSite({ repositoryNwo, githubToken, enablement = true }) {
let pageObject
// Try to find an existing Pages site first
try {
pageObject = await getPagesSite({ repositoryNwo, githubToken })
} catch (error) {
if (!enablement) {
core.error('Get Pages site failed', error)
throw error
}
core.warning('Get Pages site failed', error)
}
if (!pageObject && enablement) {
// Create a new Pages site if one doesn't exist
try {
pageObject = await enablePagesSite({ repositoryNwo, githubToken })
} catch (error) {
core.error('Create Pages site failed', error)
throw error
}
// This somehow implies that the Pages site was already created but initially failed to be retrieved.
// Try one more time for this extreme edge case!
if (pageObject == null) {
try {
pageObject = await getPagesSite({ repositoryNwo, githubToken })
} catch (error) {
core.error('Get Pages site still failed', error)
throw error
}
}
}
return pageObject
}
module.exports = { findOrCreatePagesSite, enablePagesSite, getPagesSite, getApiBaseUrl }
/***/ }), /***/ }),
/***/ 8395: /***/ 8395:
@@ -14531,161 +14528,282 @@ exports.debug = debug; // for test
const fs = __nccwpck_require__(7147) const fs = __nccwpck_require__(7147)
const espree = __nccwpck_require__(6910) const espree = __nccwpck_require__(6910)
const format = __nccwpck_require__(3259)
const core = __nccwpck_require__(2186) const core = __nccwpck_require__(2186)
// Parse the AST /*
const espreeOptions = { Parse a JavaScript based configuration file and inject arbitrary key/value in it.
ecmaVersion: 6, This is used to make sure most static site generators can automatically handle
sourceType: "module", Pages's path based routing (and work).
range: true,
} Supported configuration initializations:
(1) Default export:
export default {
// configuration object here
}
(2) Direct module export:
module.exports = {
// configuration object here
}
(3) Indirect module export:
const config = // configuration object here
module.exports = config
*/
class ConfigParser { class ConfigParser {
constructor(staticSiteConfig) { // Ctor
this.pathPropertyNuxt = `router: {\n base: '{0}'\n }` // - configurationFile: path to the configuration file
this.pathPropertyNext = `basePath: '{0}'` // - blankConfigurationFile: a blank configuration file to use if non was previously found
this.pathPropertyGatsby = `pathPrefix: '{0}'` constructor({configurationFile, blankConfigurationFile, properties}) {
this.configskeleton = `export default {\n {0}\n}` // Save field
this.staticSiteConfig = staticSiteConfig this.configurationFile = configurationFile
this.config = fs.existsSync(this.staticSiteConfig.filePath) ? fs.readFileSync(this.staticSiteConfig.filePath, "utf8") : null this.properties = properties
this.validate()
}
validate() { // If the configuration file does not exist, initialize it with the blank configuration file
if (!this.config) { if (!fs.existsSync(this.configurationFile)) {
core.info(`original raw configuration was empty:\n${this.config}`) core.info('Using default blank configuration')
core.info('Generating a default configuration to start from...') const blankConfiguration = fs.readFileSync(blankConfigurationFile, 'utf8')
fs.writeFileSync(this.configurationFile, blankConfiguration, {
// Update the `config` property with a default configuration file encoding: 'utf8'
this.config = this.generateConfigFile() })
}
}
generateConfigFile() {
switch (this.staticSiteConfig.type) {
case "nuxt":
return format(this.configskeleton, format(this.pathPropertyNuxt, this.staticSiteConfig.newPath))
break
case "next":
return format(this.configskeleton, format(this.pathPropertyNext, this.staticSiteConfig.newPath))
break
case "gatsby":
return format(this.configskeleton, format(this.pathPropertyGatsby, this.staticSiteConfig.newPath))
break
default:
throw "Unknown config type"
}
}
generateConfigProperty() {
switch (this.staticSiteConfig.type) {
case "nuxt":
return format(this.pathPropertyNuxt, this.staticSiteConfig.newPath)
break
case "next":
return format(this.pathPropertyNext, this.staticSiteConfig.newPath)
break
case "gatsby":
return format(this.pathPropertyGatsby, this.staticSiteConfig.newPath)
break
default:
throw "Unknown config type"
}
}
parse() {
core.info(`original configuration:\n${this.config}`)
const ast = espree.parse(this.config, espreeOptions);
// Find the default export declaration node
var exportNode = ast.body.find(node => node.type === 'ExpressionStatement')
if (exportNode) {
var property = this.getPropertyModuleExport(exportNode)
} else {
exportNode = ast.body.find(node => node.type === 'ExportDefaultDeclaration')
if (!exportNode) throw "Unable to find default export"
var property = this.getPropertyExportDefault(exportNode)
} }
// Read the configuration file
this.configuration = fs.readFileSync(this.configurationFile, 'utf8')
}
// Find the configuration object in an AST.
// Look for a default export, a direct module export or an indirect module
// export (in that order).
//
// Return the configuration object or null.
findConfigurationObject(ast) {
// Try to find a default export
var defaultExport = ast.body.find(
node =>
node.type === 'ExportDefaultDeclaration' &&
node.declaration.type === 'ObjectExpression'
)
if (defaultExport) {
core.info('Found configuration object in default export declaration')
return defaultExport.declaration
}
// Try to find a module export
var moduleExport = ast.body.find(
node =>
node.type === 'ExpressionStatement' &&
node.expression.type === 'AssignmentExpression' &&
node.expression.operator === '=' &&
node.expression.left.type === 'MemberExpression' &&
node.expression.left.object.type === 'Identifier' &&
node.expression.left.object.name === 'module' &&
node.expression.left.property.type === 'Identifier' &&
node.expression.left.property.name === 'exports'
)
// Direct module export
if (
moduleExport &&
moduleExport.expression.right.type === 'ObjectExpression'
) {
core.info('Found configuration object in direct module export')
return moduleExport.expression.right
}
// Indirect module export
else if (
moduleExport &&
moduleExport.expression.right.type === 'Identifier'
) {
const identifierName = moduleExport && moduleExport.expression.right.name
const identifierDefinition = ast.body.find(
node =>
node.type === 'VariableDeclaration' &&
node.declarations.length == 1 &&
node.declarations[0].type === 'VariableDeclarator' &&
node.declarations[0].id.type === 'Identifier' &&
node.declarations[0].id.name === identifierName &&
node.declarations[0].init.type === 'ObjectExpression'
)
if (identifierDefinition) {
core.info('Found configuration object in indirect module export')
return identifierDefinition.declarations[0].init
}
}
// No configuration object found
return null
}
// Find a property with a given name on a given object.
//
// Return the matching property or null.
findProperty(object, name) {
// Try to find a property matching a given name
const property =
object.type === 'ObjectExpression' &&
object.properties.find(
node => node.key.type === 'Identifier' && node.key.name === name
)
// Return the property's value (if found) or null
if (property) { if (property) {
switch (this.staticSiteConfig.type) { return property.value
case "nuxt":
this.parseNuxt(property)
break
case "next":
case "gatsby":
this.parseNextGatsby(property)
break
default:
throw "Unknown config type"
}
} }
core.info(`parsed configuration:\n${this.config}`) return null
fs.writeFileSync(this.staticSiteConfig.filePath, this.config)
return this.config
} }
getPropertyModuleExport(exportNode) { // Generate a (nested) property declaration.
var propertyNode = exportNode.expression.right.properties.find( // - properties: list of properties to generate
node => node.key.type === 'Identifier' && node.key.name === this.staticSiteConfig.pathName // - startIndex: the index at which to start in the declaration
) // - propertyValue: the value of the property
//
if (!propertyNode) { // Return a nested property declaration as a string.
getPropertyDeclaration(properties, startIndex, propertyValue) {
core.info("Unable to find property, insert it : " + this.staticSiteConfig.pathName) if (startIndex === properties.length - 1) {
if (exportNode.expression.right.properties.length > 0) { return `${properties[startIndex]}: ${JSON.stringify(propertyValue)}`
this.config = this.config.slice(0, exportNode.expression.right.properties[0].range[0]) + this.generateConfigProperty() + ',\n' + this.config.slice(exportNode.expression.right.properties[0].range[0]) } else {
core.info("new config = \n" + this.config) return (
} else { `${properties[startIndex]}: {` +
this.config = this.config.slice(0, exportNode.expression.right.range[0] + 1) + '\n ' + this.generateConfigProperty() + '\n' + this.config.slice(exportNode.expression.right.range[1] - 1) this.getPropertyDeclaration(properties, startIndex + 1, propertyValue) +
core.info("new config = \n" + this.config) '}'
} )
}
return propertyNode
}
getPropertyExportDefault(exportNode) {
var propertyNode = exportNode.declaration.properties.find(
node => node.key.type === 'Identifier' && node.key.name === this.staticSiteConfig.pathName
)
if (!propertyNode) {
core.info("Unable to find property, insert it " + this.staticSiteConfig.pathName)
if (exportNode.declaration.properties.length > 0) {
this.config = this.config.slice(0, exportNode.declaration.properties[0].range[0]) + this.generateConfigProperty() + ',\n' + this.config.slice(exportNode.declaration.properties[0].range[0])
core.info("new config = \n" + this.config)
} else {
this.config = this.config.slice(0, exportNode.declaration.range[0] + 1) + '\n ' + this.generateConfigProperty() + '\n' + this.config.slice(exportNode.declaration.range[1] - 1)
core.info("new config = \n" + this.config)
}
}
return propertyNode
}
parseNuxt(propertyNode) {
// Find the base node
if (propertyNode && propertyNode.value.type === 'ObjectExpression') {
var baseNode = propertyNode.value.properties.find(node => node.key.type === 'Identifier' && node.key.name === this.staticSiteConfig.subPathName)//'base')
if (baseNode) {
// Swap the base value by a hardcoded string and print it
this.config = this.config.slice(0, baseNode.value.range[0]) + `'${this.staticSiteConfig.newPath}'` + this.config.slice(baseNode.value.range[1])
}
} }
} }
parseNextGatsby(pathNode) { // Inject all properties into the configuration
if (pathNode) { injectAll() {
this.config = this.config.slice(0, pathNode.value.range[0]) + `'${this.staticSiteConfig.newPath}'` + this.config.slice(pathNode.value.range[1]) for (var [propertyName, propertyValue] of Object.entries(this.properties)) {
this.inject(propertyName, propertyValue)
} }
} }
// Inject an arbitrary property into the configuration
// - propertyName: the name of the property (may use . to target nested objects)
// - propertyValue: the value of the property
inject(propertyName, propertyValue) {
// Logging
core.info(`Injecting property=${propertyName} and value=${propertyValue} in:`)
core.info(this.configuration)
// Parse the AST out of the configuration file
const espreeOptions = {
ecmaVersion: 'latest',
sourceType: 'module',
range: true
}
const ast = espree.parse(this.configuration, espreeOptions)
// Find the configuration object
var configurationObject = this.findConfigurationObject(ast)
if (!configurationObject) {
throw 'Could not find a configuration object in the configuration file'
}
// A property may be nested in the configuration file. Split the property name with `.`
// then walk the configuration object one property at a time.
var depth = 0
const properties = propertyName.split('.')
var lastNode = configurationObject
while (1) {
// Find the node for the current property
var propertyNode = this.findProperty(lastNode, properties[depth])
// Update last node
if (propertyNode != null) {
lastNode = propertyNode
depth++
}
// Exit when exiting the current configuration object
if (propertyNode == null || depth >= properties.length) {
break
}
}
// If the configuration file is defining the property we are after, update it.
if (depth == properties.length) {
// The last node identified is an object expression, so do the assignment
if (lastNode.type === 'ObjectExpression') {
this.configuration =
this.configuration.slice(0, lastNode.range[0]) +
JSON.stringify(propertyValue) +
this.configuration.slice(lastNode.range[1])
}
// A misc object was found in the configuration file (e.g. an array, a string, a boolean,
// a number, etc.), just replace the whole range by our declaration
else {
this.configuration =
this.configuration.slice(0, lastNode.range[0]) +
JSON.stringify(propertyValue) +
this.configuration.slice(lastNode.range[1])
}
}
// Create nested properties in the configuration file
else {
// Build the declaration to inject
const declaration = this.getPropertyDeclaration(
properties,
depth,
propertyValue
)
// The last node identified is an object expression, so do the assignment
if (lastNode.type === 'ObjectExpression') {
// The object is blank (no properties) so replace the whole range by a new object containing the declaration
if (lastNode.properties.length === 0) {
this.configuration =
this.configuration.slice(0, lastNode.range[0]) +
'{' +
declaration +
'}' +
this.configuration.slice(lastNode.range[1])
}
// The object contains other properties, prepend our new one at the beginning
else {
this.configuration =
this.configuration.slice(0, lastNode.properties[0].range[0]) +
declaration +
',' +
this.configuration.slice(lastNode.properties[0].range[0])
}
}
// A misc object was found in the configuration file (e.g. an array, a string, a boolean,
// a number, etc.), just replace the whole range by our declaration
else {
this.configuration =
this.configuration.slice(0, lastNode.range[0]) +
'{' +
declaration +
'}' +
this.configuration.slice(lastNode.range[1])
}
}
// Logging
core.info(`Injection successful, new configuration:`)
core.info(this.configuration)
// Finally write the new configuration in the file
fs.writeFileSync(this.configurationFile, this.configuration, {
encoding: 'utf8'
})
}
} }
module.exports = {ConfigParser} module.exports = {ConfigParser}
/***/ }), /***/ }),
/***/ 1319: /***/ 1319:
@@ -14698,11 +14816,13 @@ function getRequiredVars() {
return { return {
repositoryNwo: process.env.GITHUB_REPOSITORY, repositoryNwo: process.env.GITHUB_REPOSITORY,
githubToken: core.getInput('token'), githubToken: core.getInput('token'),
staticSiteGenerator: core.getInput('static_site_generator') staticSiteGenerator: core.getInput('static_site_generator'),
enablement: core.getInput('enablement') !== 'false'
} }
} }
module.exports = function getContext() { // Return the context object
function getContext() {
const requiredVars = getRequiredVars() const requiredVars = getRequiredVars()
for (const variable in requiredVars) { for (const variable in requiredVars) {
if (requiredVars[variable] === undefined) { if (requiredVars[variable] === undefined) {
@@ -14713,87 +14833,24 @@ module.exports = function getContext() {
return requiredVars return requiredVars
} }
module.exports = {getContext}
/***/ }),
/***/ 5424:
/***/ ((module, __unused_webpack_exports, __nccwpck_require__) => {
const core = __nccwpck_require__(2186)
const axios = __nccwpck_require__(6545)
async function enablePages({ repositoryNwo, githubToken }) {
const pagesEndpoint = `https://api.github.com/repos/${repositoryNwo}/pages`
try {
const response = await axios.post(
pagesEndpoint,
{ build_type: 'workflow' },
{
headers: {
Accept: 'application/vnd.github.v3+json',
Authorization: `Bearer ${githubToken}`,
'Content-type': 'application/json',
},
}
)
core.info('Created pages site')
} catch (error) {
if (error.response && error.response.status === 409) {
core.info('Pages site exists')
return
}
core.error('Couldn\'t create pages site', error)
throw error
}
}
module.exports = enablePages
/***/ }), /***/ }),
/***/ 9965: /***/ 7527:
/***/ ((module, __unused_webpack_exports, __nccwpck_require__) => { /***/ ((module, __unused_webpack_exports, __nccwpck_require__) => {
const core = __nccwpck_require__(2186) const core = __nccwpck_require__(2186)
const axios = __nccwpck_require__(6545)
const setPagesPath = __nccwpck_require__(4770)
async function getPagesBaseUrl({ repositoryNwo, githubToken, staticSiteGenerator}) { function outputPagesBaseUrl(siteUrl) {
try { core.setOutput('base_url', siteUrl.href)
const pagesEndpoint = `https://api.github.com/repos/${repositoryNwo}/pages` core.setOutput('origin', siteUrl.origin)
core.setOutput('host', siteUrl.host)
core.info(`Get the Base URL to the page with endpoint ${pagesEndpoint}`) core.setOutput('base_path', siteUrl.pathname)
const response = await axios.get(
pagesEndpoint,
{
headers: {
Accept: 'application/vnd.github.v3+json',
Authorization: `Bearer ${githubToken}`
}
}
)
pageObject = response.data
core.info(JSON.stringify(pageObject))
const siteUrl = new URL(pageObject.html_url)
if ( staticSiteGenerator ) {
setPagesPath({staticSiteGenerator, path: siteUrl.pathname})
}
core.setOutput('base_url', siteUrl.href)
core.setOutput('origin', siteUrl.origin)
core.setOutput('host', siteUrl.host)
core.setOutput('base_path', siteUrl.pathname)
} catch (error) {
core.error('Get on the Page failed', error)
throw error
}
} }
module.exports = getPagesBaseUrl module.exports = outputPagesBaseUrl
/***/ }), /***/ }),
@@ -14802,51 +14859,73 @@ module.exports = getPagesBaseUrl
/***/ ((module, __unused_webpack_exports, __nccwpck_require__) => { /***/ ((module, __unused_webpack_exports, __nccwpck_require__) => {
const core = __nccwpck_require__(2186) const core = __nccwpck_require__(2186)
const axios = __nccwpck_require__(6545) const {ConfigParser} = __nccwpck_require__(8395)
const { ConfigParser } = __nccwpck_require__(8395)
async function setPagesPath({staticSiteGenerator, path}) { // Return the settings to be passed to a {ConfigParser} for a given
try { // static site generator and a Pages path value to inject
switch(staticSiteGenerator) function getConfigParserSettings(staticSiteGenerator, path) {
{ switch (staticSiteGenerator) {
case 'nuxt': case 'nuxt':
var ssConfig = { return {
filePath:"./nuxt.config.js", configurationFile: './nuxt.config.js',
type: "nuxt", blankConfigurationFile: __nccwpck_require__.ab + "nuxt.js",
pathName: "router", properties: {
subPathName: "base", // Configure a base path on the router
newPath: path 'router.base': path,
}
break;
case 'next':
var ssConfig = {
filePath:"./next.config.js",
type: "next",
pathName: "basePath",
newPath: path
}
break;
case 'gatsby':
var ssConfig = {
filePath: "./gatsby-config.js",
type: "gatsby",
pathName: "pathPrefix",
newPath: path
}
break;
default:
throw "Unknown config type"
}
let configParser = new ConfigParser(ssConfig) // Set the target to static too
configParser.parse() // https://nuxtjs.org/docs/configuration-glossary/configuration-target/
target: 'static'
}
}
case 'next':
// Next does not want a trailing slash
if (path.endsWith('/')) {
path = path.slice(0, -1)
}
} catch (error) { return {
core.warning(`We were unable to determine how to inject the site metadata into your config. Generated URLs may be incorrect. The base URL for this site should be ${path}. Please ensure your framework is configured to generate relative links appropriately.`, error) configurationFile: './next.config.js',
blankConfigurationFile: __nccwpck_require__.ab + "next.js",
properties: {
// Configure a base path
basePath: path,
// Disable server side image optimization too
// https://nextjs.org/docs/api-reference/next/image#unoptimized
'experimental.images.unoptimized': true
}
}
case 'gatsby':
return {
configurationFile: './gatsby-config.js',
blankConfigurationFile: __nccwpck_require__.ab + "gatsby.js",
properties: {
// Configure a path prefix
pathPrefix: path
}
}
default:
throw `Unsupported static site generator: ${staticSiteGenerator}`
} }
} }
module.exports = setPagesPath // Inject Pages configuration in a given static site generator's configuration file
function setPagesPath({staticSiteGenerator, path}) {
try {
// Parse the configuration file and try to inject the Pages configuration in it
const settings = getConfigParserSettings(staticSiteGenerator, path)
new ConfigParser(settings).injectAll()
} catch (error) {
// Logging
core.warning(
`We were unable to determine how to inject the site metadata into your config. Generated URLs may be incorrect. The base URL for this site should be ${path}. Please ensure your framework is configured to generate relative links appropriately.`,
error
)
}
}
module.exports = {getConfigParserSettings, setPagesPath}
/***/ }), /***/ }),
@@ -16329,24 +16408,30 @@ var __webpack_exports__ = {};
(() => { (() => {
const core = __nccwpck_require__(2186) const core = __nccwpck_require__(2186)
const enablePages = __nccwpck_require__(5424)
const getPagesBaseUrl = __nccwpck_require__(9965)
// All variables we need from the runtime are loaded here // All variables we need from the runtime are loaded here
const getContext = __nccwpck_require__(1319) const { getContext } = __nccwpck_require__(1319)
const { findOrCreatePagesSite } = __nccwpck_require__(9432)
const { setPagesPath } = __nccwpck_require__(4770)
const outputPagesBaseUrl = __nccwpck_require__(7527)
async function main() { async function main() {
try { try {
const context = getContext() const { repositoryNwo, githubToken, enablement, staticSiteGenerator } = getContext()
await enablePages(context)
await getPagesBaseUrl(context) const pageObject = await findOrCreatePagesSite({ repositoryNwo, githubToken, enablement })
const siteUrl = new URL(pageObject.html_url)
if (staticSiteGenerator) {
setPagesPath({ staticSiteGenerator, path: siteUrl.pathname })
}
outputPagesBaseUrl(siteUrl)
} catch (error) { } catch (error) {
core.setFailed(error) core.setFailed(error)
process.exit(1) process.exit(1)
} }
} }
// Main // Main
main() main()

2
dist/index.js.map vendored

File diff suppressed because one or more lines are too long

18
dist/licenses.txt vendored
View File

@@ -572,24 +572,6 @@ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE. SOFTWARE.
string-format
(WTFPL OR MIT)
DO WHAT THE FUCK YOU WANT TO PUBLIC LICENSE
Version 2, December 2004
Copyright (c) 2018 David Chambers <dc@davidchambers.me>
Everyone is permitted to copy and distribute verbatim or modified
copies of this license document, and changing it is allowed as long
as the name is changed.
DO WHAT THE FUCK YOU WANT TO PUBLIC LICENSE
TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION
0. You just DO WHAT THE FUCK YOU WANT TO.
supports-color supports-color
MIT MIT
MIT License MIT License

3
dist/next.js vendored Normal file
View File

@@ -0,0 +1,3 @@
// Default Pages configuration for Next
const nextConfig = {}
module.exports = nextConfig

2
dist/nuxt.js vendored Normal file
View File

@@ -0,0 +1,2 @@
// Default Pages configuration for Nuxt
export default {}

24
package-lock.json generated
View File

@@ -17,7 +17,8 @@
}, },
"devDependencies": { "devDependencies": {
"@vercel/ncc": "^0.34.0", "@vercel/ncc": "^0.34.0",
"jest": "^28.1.1" "jest": "^28.1.1",
"prettier": "^2.7.1"
} }
}, },
"node_modules/@actions/core": { "node_modules/@actions/core": {
@@ -3041,6 +3042,21 @@
"node": ">=8" "node": ">=8"
} }
}, },
"node_modules/prettier": {
"version": "2.7.1",
"resolved": "https://registry.npmjs.org/prettier/-/prettier-2.7.1.tgz",
"integrity": "sha512-ujppO+MkdPqoVINuDFDRLClm7D78qbDt0/NR+wp5FqEZOoTNAjPHWj17QRhu7geIHJfcNhRk1XVQmF8Bp3ye+g==",
"dev": true,
"bin": {
"prettier": "bin-prettier.js"
},
"engines": {
"node": ">=10.13.0"
},
"funding": {
"url": "https://github.com/prettier/prettier?sponsor=1"
}
},
"node_modules/pretty-format": { "node_modules/pretty-format": {
"version": "28.1.1", "version": "28.1.1",
"resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-28.1.1.tgz", "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-28.1.1.tgz",
@@ -5878,6 +5894,12 @@
"find-up": "^4.0.0" "find-up": "^4.0.0"
} }
}, },
"prettier": {
"version": "2.7.1",
"resolved": "https://registry.npmjs.org/prettier/-/prettier-2.7.1.tgz",
"integrity": "sha512-ujppO+MkdPqoVINuDFDRLClm7D78qbDt0/NR+wp5FqEZOoTNAjPHWj17QRhu7geIHJfcNhRk1XVQmF8Bp3ye+g==",
"dev": true
},
"pretty-format": { "pretty-format": {
"version": "28.1.1", "version": "28.1.1",
"resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-28.1.1.tgz", "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-28.1.1.tgz",

View File

@@ -1,22 +1,23 @@
{ {
"private": true,
"name": "configure-pages", "name": "configure-pages",
"version": "1.0.0", "version": "1.0.0",
"description": "", "description": "A GitHub Action to enable Pages and extract various metadata about a site. It can also be used to configure various static site generators we support as starter workflows.",
"main": "src/index.js", "main": "./dist/index.js",
"scripts": { "scripts": {
"prepare": "ncc build src/index.js -o dist --source-map --license licenses.txt", "prepare": "ncc build src/index.js -o dist --source-map --license licenses.txt",
"test": "jest" "test": "jest"
}, },
"repository": { "repository": {
"type": "git", "type": "git",
"url": "git+https://github.com/paper-spa/configure-pages.git" "url": "git+https://github.com/actions/configure-pages.git"
}, },
"author": "GitHub", "author": "GitHub",
"license": "MIT", "license": "MIT",
"bugs": { "bugs": {
"url": "https://github.com/paper-spa/configure-pages/issues" "url": "https://github.com/actions/configure-pages/issues"
}, },
"homepage": "https://github.com/paper-spa/configure-pages#readme", "homepage": "https://github.com/actions/configure-pages#readme",
"dependencies": { "dependencies": {
"@actions/core": "^1.8.2", "@actions/core": "^1.8.2",
"axios": "^0.27.2", "axios": "^0.27.2",
@@ -26,6 +27,7 @@
}, },
"devDependencies": { "devDependencies": {
"@vercel/ncc": "^0.34.0", "@vercel/ncc": "^0.34.0",
"jest": "^28.1.1" "jest": "^28.1.1",
"prettier": "^2.7.1"
} }
} }

91
src/api-client.js Normal file
View File

@@ -0,0 +1,91 @@
const axios = require('axios')
const core = require('@actions/core')
function getApiBaseUrl() {
return process.env.GITHUB_API_URL || 'https://api.github.com'
}
async function enablePagesSite({ repositoryNwo, githubToken }) {
const pagesEndpoint = `${getApiBaseUrl()}/repos/${repositoryNwo}/pages`
try {
const response = await axios.post(
pagesEndpoint,
{ build_type: 'workflow' },
{
headers: {
Accept: 'application/vnd.github.v3+json',
Authorization: `Bearer ${githubToken}`,
'Content-type': 'application/json'
}
}
)
const pageObject = response.data
return pageObject
} catch (error) {
if (error.response && error.response.status === 409) {
return null
}
throw error
}
}
async function getPagesSite({ repositoryNwo, githubToken }) {
try {
const pagesEndpoint = `${getApiBaseUrl()}/repos/${repositoryNwo}/pages`
const response = await axios.get(pagesEndpoint, {
headers: {
Accept: 'application/vnd.github.v3+json',
Authorization: `Bearer ${githubToken}`
}
})
const pageObject = response.data
return pageObject
} catch (error) {
throw error
}
}
async function findOrCreatePagesSite({ repositoryNwo, githubToken, enablement = true }) {
let pageObject
// Try to find an existing Pages site first
try {
pageObject = await getPagesSite({ repositoryNwo, githubToken })
} catch (error) {
if (!enablement) {
core.error('Get Pages site failed', error)
throw error
}
core.warning('Get Pages site failed', error)
}
if (!pageObject && enablement) {
// Create a new Pages site if one doesn't exist
try {
pageObject = await enablePagesSite({ repositoryNwo, githubToken })
} catch (error) {
core.error('Create Pages site failed', error)
throw error
}
// This somehow implies that the Pages site was already created but initially failed to be retrieved.
// Try one more time for this extreme edge case!
if (pageObject == null) {
try {
pageObject = await getPagesSite({ repositoryNwo, githubToken })
} catch (error) {
core.error('Get Pages site still failed', error)
throw error
}
}
}
return pageObject
}
module.exports = { findOrCreatePagesSite, enablePagesSite, getPagesSite, getApiBaseUrl }

221
src/api-client.test.js Normal file
View File

@@ -0,0 +1,221 @@
const core = require('@actions/core')
const axios = require('axios')
const apiClient = require('./api-client')
describe('apiClient', () => {
const GITHUB_REPOSITORY = 'actions/is-awesome'
const GITHUB_TOKEN = 'gha-token'
const PAGE_OBJECT = { html_url: 'https://actions.github.io/is-awesome/' }
beforeEach(() => {
jest.restoreAllMocks()
// Mock error/warning/info/debug
jest.spyOn(core, 'error').mockImplementation(jest.fn())
jest.spyOn(core, 'warning').mockImplementation(jest.fn())
jest.spyOn(core, 'info').mockImplementation(jest.fn())
jest.spyOn(core, 'debug').mockImplementation(jest.fn())
})
describe('getApiBaseUrl', () => {
it('returns GITHUB_API_URL environment variable when set', async () => {
const expectedBaseUrl = 'https://api.ghe.com'
process.env.GITHUB_API_URL = expectedBaseUrl
const result = apiClient.getApiBaseUrl()
delete process.env.GITHUB_API_URL
expect(result).toEqual(expectedBaseUrl)
})
it('defaults to GitHub API if GITHUB_API_URL environment variable is empty', async () => {
process.env.GITHUB_API_URL = ''
const result = apiClient.getApiBaseUrl()
delete process.env.GITHUB_API_URL
expect(result).toEqual('https://api.github.com')
})
it('defaults to GitHub API if GITHUB_API_URL environment variable is not set', async () => {
delete process.env.GITHUB_API_URL
const result = apiClient.getApiBaseUrl()
expect(result).toEqual('https://api.github.com')
})
})
describe('enablePagesSite', () => {
it('makes a request to create a page', async () => {
jest.spyOn(axios, 'post').mockImplementationOnce(() => Promise.resolve({ status: 201, data: PAGE_OBJECT }))
const result = await apiClient.enablePagesSite({
repositoryNwo: GITHUB_REPOSITORY,
githubToken: GITHUB_TOKEN
})
expect(result).toEqual(PAGE_OBJECT)
})
it('handles a 409 response when the page already exists', async () => {
jest
.spyOn(axios, 'post')
.mockImplementationOnce(() => Promise.reject({ response: { status: 409 } }))
// Simply assert that no error is raised
const result = await apiClient.enablePagesSite({
repositoryNwo: GITHUB_REPOSITORY,
githubToken: GITHUB_TOKEN
})
expect(result).toBe(null)
})
it('re-raises errors on failure status codes', async () => {
jest
.spyOn(axios, 'post')
.mockImplementationOnce(() => Promise.reject({ response: { status: 404 } }))
let erred = false
try {
await apiClient.enablePagesSite({
repositoryNwo: GITHUB_REPOSITORY,
githubToken: GITHUB_TOKEN
})
} catch (error) {
erred = true
expect(error.response.status).toEqual(404)
}
expect(erred).toBe(true)
})
})
describe('getPagesSite', () => {
it('makes a request to get a page', async () => {
const PAGE_OBJECT = { html_url: 'https://actions.github.io/is-awesome/' }
jest.spyOn(axios, 'get').mockImplementationOnce(() => Promise.resolve({ status: 200, data: PAGE_OBJECT }))
const result = await apiClient.getPagesSite({
repositoryNwo: GITHUB_REPOSITORY,
githubToken: GITHUB_TOKEN
})
expect(result).toEqual(PAGE_OBJECT)
})
it('re-raises errors on failure status codes', async () => {
jest
.spyOn(axios, 'get')
.mockImplementationOnce(() => Promise.reject({ response: { status: 404 } }))
let erred = false
try {
await apiClient.getPagesSite({
repositoryNwo: GITHUB_REPOSITORY,
githubToken: GITHUB_TOKEN
})
} catch (error) {
erred = true
expect(error.response.status).toEqual(404)
}
expect(erred).toBe(true)
})
})
describe('findOrCreatePagesSite', () => {
it('does not make a request to create a page if it already exists', async () => {
const PAGE_OBJECT = { html_url: 'https://actions.github.io/is-awesome/' }
jest.spyOn(axios, 'get').mockImplementationOnce(() => Promise.resolve({ status: 200, data: PAGE_OBJECT }))
jest.spyOn(axios, 'post').mockImplementationOnce(() => Promise.reject({ response: { status: 404 } }))
const result = await apiClient.findOrCreatePagesSite({
repositoryNwo: GITHUB_REPOSITORY,
githubToken: GITHUB_TOKEN
})
expect(result).toEqual(PAGE_OBJECT)
expect(axios.get).toHaveBeenCalledTimes(1)
expect(axios.post).toHaveBeenCalledTimes(0)
})
it('makes request to create a page by default if it does not exist', async () => {
const PAGE_OBJECT = { html_url: 'https://actions.github.io/is-awesome/' }
jest.spyOn(axios, 'get').mockImplementationOnce(() => Promise.reject({ response: { status: 404 } }))
jest.spyOn(axios, 'post').mockImplementationOnce(() => Promise.resolve({ status: 201, data: PAGE_OBJECT }))
const result = await apiClient.findOrCreatePagesSite({
repositoryNwo: GITHUB_REPOSITORY,
githubToken: GITHUB_TOKEN
})
expect(result).toEqual(PAGE_OBJECT)
expect(axios.get).toHaveBeenCalledTimes(1)
expect(axios.post).toHaveBeenCalledTimes(1)
})
it('makes a request to create a page when explicitly enabled if it does not exist', async () => {
const PAGE_OBJECT = { html_url: 'https://actions.github.io/is-awesome/' }
jest.spyOn(axios, 'get').mockImplementationOnce(() => Promise.reject({ response: { status: 404 } }))
jest.spyOn(axios, 'post').mockImplementationOnce(() => Promise.resolve({ status: 201, data: PAGE_OBJECT }))
const result = await apiClient.findOrCreatePagesSite({
repositoryNwo: GITHUB_REPOSITORY,
githubToken: GITHUB_TOKEN,
enablement: true
})
expect(result).toEqual(PAGE_OBJECT)
expect(axios.get).toHaveBeenCalledTimes(1)
expect(axios.post).toHaveBeenCalledTimes(1)
})
it('does not make a request to create a page when explicitly disabled even if it does not exist', async () => {
jest.spyOn(axios, 'get').mockImplementationOnce(() => Promise.reject({ response: { status: 404 } }))
jest.spyOn(axios, 'post').mockImplementationOnce(() => Promise.reject({ response: { status: 500 } })) // just so they both aren't 404
let erred = false
try {
await apiClient.findOrCreatePagesSite({
repositoryNwo: GITHUB_REPOSITORY,
githubToken: GITHUB_TOKEN,
enablement: false
})
} catch (error) {
erred = true
// re-raised error
expect(error.response.status).toEqual(404)
}
expect(erred).toBe(true)
expect(axios.get).toHaveBeenCalledTimes(1)
expect(axios.post).toHaveBeenCalledTimes(0)
})
it('does not make a second request to get page if create fails for reason other than existence', async () => {
jest.spyOn(axios, 'get').mockImplementationOnce(() => Promise.reject({ response: { status: 404 } }))
jest.spyOn(axios, 'post').mockImplementationOnce(() => Promise.reject({ response: { status: 500 } })) // just so they both aren't 404
let erred = false
try {
await apiClient.findOrCreatePagesSite({
repositoryNwo: GITHUB_REPOSITORY,
githubToken: GITHUB_TOKEN
})
} catch (error) {
erred = true
// re-raised error
expect(error.response.status).toEqual(500)
}
expect(erred).toBe(true)
expect(axios.get).toHaveBeenCalledTimes(1)
expect(axios.post).toHaveBeenCalledTimes(1)
})
it('makes second request to get page if create fails because of existence', async () => {
const PAGE_OBJECT = { html_url: 'https://actions.github.io/is-awesome/' }
jest.spyOn(axios, 'get')
.mockImplementationOnce(() => Promise.reject({ response: { status: 404 } }))
.mockImplementationOnce(() => Promise.resolve({ status: 200, data: PAGE_OBJECT }))
jest.spyOn(axios, 'post').mockImplementationOnce(() => Promise.reject({ response: { status: 409 } }))
const result = await apiClient.findOrCreatePagesSite({
repositoryNwo: GITHUB_REPOSITORY,
githubToken: GITHUB_TOKEN
})
expect(result).toEqual(PAGE_OBJECT)
expect(axios.get).toHaveBeenCalledTimes(2)
expect(axios.post).toHaveBeenCalledTimes(1)
})
})
})

View File

@@ -0,0 +1,2 @@
// Default Pages configuration for Gatsby
module.exports = {}

View File

@@ -0,0 +1,3 @@
// Default Pages configuration for Next
const nextConfig = {}
module.exports = nextConfig

View File

@@ -0,0 +1,2 @@
// Default Pages configuration for Nuxt
export default {}

View File

@@ -1,156 +1,276 @@
const fs = require("fs") const fs = require('fs')
const espree = require("espree") const espree = require('espree')
const format = require("string-format")
const core = require('@actions/core') const core = require('@actions/core')
// Parse the AST /*
const espreeOptions = { Parse a JavaScript based configuration file and inject arbitrary key/value in it.
ecmaVersion: 6, This is used to make sure most static site generators can automatically handle
sourceType: "module", Pages's path based routing (and work).
range: true,
} Supported configuration initializations:
(1) Default export:
export default {
// configuration object here
}
(2) Direct module export:
module.exports = {
// configuration object here
}
(3) Indirect module export:
const config = // configuration object here
module.exports = config
*/
class ConfigParser { class ConfigParser {
constructor(staticSiteConfig) { // Ctor
this.pathPropertyNuxt = `router: {\n base: '{0}'\n }` // - configurationFile: path to the configuration file
this.pathPropertyNext = `basePath: '{0}'` // - blankConfigurationFile: a blank configuration file to use if non was previously found
this.pathPropertyGatsby = `pathPrefix: '{0}'` constructor({configurationFile, blankConfigurationFile, properties}) {
this.configskeleton = `export default {\n {0}\n}` // Save field
this.staticSiteConfig = staticSiteConfig this.configurationFile = configurationFile
this.config = fs.existsSync(this.staticSiteConfig.filePath) ? fs.readFileSync(this.staticSiteConfig.filePath, "utf8") : null this.properties = properties
this.validate()
}
validate() { // If the configuration file does not exist, initialize it with the blank configuration file
if (!this.config) { if (!fs.existsSync(this.configurationFile)) {
core.info(`original raw configuration was empty:\n${this.config}`) core.info('Using default blank configuration')
core.info('Generating a default configuration to start from...') const blankConfiguration = fs.readFileSync(blankConfigurationFile, 'utf8')
fs.writeFileSync(this.configurationFile, blankConfiguration, {
// Update the `config` property with a default configuration file encoding: 'utf8'
this.config = this.generateConfigFile() })
}
}
generateConfigFile() {
switch (this.staticSiteConfig.type) {
case "nuxt":
return format(this.configskeleton, format(this.pathPropertyNuxt, this.staticSiteConfig.newPath))
break
case "next":
return format(this.configskeleton, format(this.pathPropertyNext, this.staticSiteConfig.newPath))
break
case "gatsby":
return format(this.configskeleton, format(this.pathPropertyGatsby, this.staticSiteConfig.newPath))
break
default:
throw "Unknown config type"
}
}
generateConfigProperty() {
switch (this.staticSiteConfig.type) {
case "nuxt":
return format(this.pathPropertyNuxt, this.staticSiteConfig.newPath)
break
case "next":
return format(this.pathPropertyNext, this.staticSiteConfig.newPath)
break
case "gatsby":
return format(this.pathPropertyGatsby, this.staticSiteConfig.newPath)
break
default:
throw "Unknown config type"
}
}
parse() {
core.info(`original configuration:\n${this.config}`)
const ast = espree.parse(this.config, espreeOptions);
// Find the default export declaration node
var exportNode = ast.body.find(node => node.type === 'ExpressionStatement')
if (exportNode) {
var property = this.getPropertyModuleExport(exportNode)
} else {
exportNode = ast.body.find(node => node.type === 'ExportDefaultDeclaration')
if (!exportNode) throw "Unable to find default export"
var property = this.getPropertyExportDefault(exportNode)
} }
// Read the configuration file
this.configuration = fs.readFileSync(this.configurationFile, 'utf8')
}
// Find the configuration object in an AST.
// Look for a default export, a direct module export or an indirect module
// export (in that order).
//
// Return the configuration object or null.
findConfigurationObject(ast) {
// Try to find a default export
var defaultExport = ast.body.find(
node =>
node.type === 'ExportDefaultDeclaration' &&
node.declaration.type === 'ObjectExpression'
)
if (defaultExport) {
core.info('Found configuration object in default export declaration')
return defaultExport.declaration
}
// Try to find a module export
var moduleExport = ast.body.find(
node =>
node.type === 'ExpressionStatement' &&
node.expression.type === 'AssignmentExpression' &&
node.expression.operator === '=' &&
node.expression.left.type === 'MemberExpression' &&
node.expression.left.object.type === 'Identifier' &&
node.expression.left.object.name === 'module' &&
node.expression.left.property.type === 'Identifier' &&
node.expression.left.property.name === 'exports'
)
// Direct module export
if (
moduleExport &&
moduleExport.expression.right.type === 'ObjectExpression'
) {
core.info('Found configuration object in direct module export')
return moduleExport.expression.right
}
// Indirect module export
else if (
moduleExport &&
moduleExport.expression.right.type === 'Identifier'
) {
const identifierName = moduleExport && moduleExport.expression.right.name
const identifierDefinition = ast.body.find(
node =>
node.type === 'VariableDeclaration' &&
node.declarations.length == 1 &&
node.declarations[0].type === 'VariableDeclarator' &&
node.declarations[0].id.type === 'Identifier' &&
node.declarations[0].id.name === identifierName &&
node.declarations[0].init.type === 'ObjectExpression'
)
if (identifierDefinition) {
core.info('Found configuration object in indirect module export')
return identifierDefinition.declarations[0].init
}
}
// No configuration object found
return null
}
// Find a property with a given name on a given object.
//
// Return the matching property or null.
findProperty(object, name) {
// Try to find a property matching a given name
const property =
object.type === 'ObjectExpression' &&
object.properties.find(
node => node.key.type === 'Identifier' && node.key.name === name
)
// Return the property's value (if found) or null
if (property) { if (property) {
switch (this.staticSiteConfig.type) { return property.value
case "nuxt":
this.parseNuxt(property)
break
case "next":
case "gatsby":
this.parseNextGatsby(property)
break
default:
throw "Unknown config type"
}
} }
core.info(`parsed configuration:\n${this.config}`) return null
fs.writeFileSync(this.staticSiteConfig.filePath, this.config)
return this.config
} }
getPropertyModuleExport(exportNode) { // Generate a (nested) property declaration.
var propertyNode = exportNode.expression.right.properties.find( // - properties: list of properties to generate
node => node.key.type === 'Identifier' && node.key.name === this.staticSiteConfig.pathName // - startIndex: the index at which to start in the declaration
) // - propertyValue: the value of the property
//
if (!propertyNode) { // Return a nested property declaration as a string.
getPropertyDeclaration(properties, startIndex, propertyValue) {
core.info("Unable to find property, insert it : " + this.staticSiteConfig.pathName) if (startIndex === properties.length - 1) {
if (exportNode.expression.right.properties.length > 0) { return `${properties[startIndex]}: ${JSON.stringify(propertyValue)}`
this.config = this.config.slice(0, exportNode.expression.right.properties[0].range[0]) + this.generateConfigProperty() + ',\n' + this.config.slice(exportNode.expression.right.properties[0].range[0]) } else {
core.info("new config = \n" + this.config) return (
} else { `${properties[startIndex]}: {` +
this.config = this.config.slice(0, exportNode.expression.right.range[0] + 1) + '\n ' + this.generateConfigProperty() + '\n' + this.config.slice(exportNode.expression.right.range[1] - 1) this.getPropertyDeclaration(properties, startIndex + 1, propertyValue) +
core.info("new config = \n" + this.config) '}'
} )
}
return propertyNode
}
getPropertyExportDefault(exportNode) {
var propertyNode = exportNode.declaration.properties.find(
node => node.key.type === 'Identifier' && node.key.name === this.staticSiteConfig.pathName
)
if (!propertyNode) {
core.info("Unable to find property, insert it " + this.staticSiteConfig.pathName)
if (exportNode.declaration.properties.length > 0) {
this.config = this.config.slice(0, exportNode.declaration.properties[0].range[0]) + this.generateConfigProperty() + ',\n' + this.config.slice(exportNode.declaration.properties[0].range[0])
core.info("new config = \n" + this.config)
} else {
this.config = this.config.slice(0, exportNode.declaration.range[0] + 1) + '\n ' + this.generateConfigProperty() + '\n' + this.config.slice(exportNode.declaration.range[1] - 1)
core.info("new config = \n" + this.config)
}
}
return propertyNode
}
parseNuxt(propertyNode) {
// Find the base node
if (propertyNode && propertyNode.value.type === 'ObjectExpression') {
var baseNode = propertyNode.value.properties.find(node => node.key.type === 'Identifier' && node.key.name === this.staticSiteConfig.subPathName)//'base')
if (baseNode) {
// Swap the base value by a hardcoded string and print it
this.config = this.config.slice(0, baseNode.value.range[0]) + `'${this.staticSiteConfig.newPath}'` + this.config.slice(baseNode.value.range[1])
}
} }
} }
parseNextGatsby(pathNode) { // Inject all properties into the configuration
if (pathNode) { injectAll() {
this.config = this.config.slice(0, pathNode.value.range[0]) + `'${this.staticSiteConfig.newPath}'` + this.config.slice(pathNode.value.range[1]) for (var [propertyName, propertyValue] of Object.entries(this.properties)) {
this.inject(propertyName, propertyValue)
} }
} }
// Inject an arbitrary property into the configuration
// - propertyName: the name of the property (may use . to target nested objects)
// - propertyValue: the value of the property
inject(propertyName, propertyValue) {
// Logging
core.info(`Injecting property=${propertyName} and value=${propertyValue} in:`)
core.info(this.configuration)
// Parse the AST out of the configuration file
const espreeOptions = {
ecmaVersion: 'latest',
sourceType: 'module',
range: true
}
const ast = espree.parse(this.configuration, espreeOptions)
// Find the configuration object
var configurationObject = this.findConfigurationObject(ast)
if (!configurationObject) {
throw 'Could not find a configuration object in the configuration file'
}
// A property may be nested in the configuration file. Split the property name with `.`
// then walk the configuration object one property at a time.
var depth = 0
const properties = propertyName.split('.')
var lastNode = configurationObject
while (1) {
// Find the node for the current property
var propertyNode = this.findProperty(lastNode, properties[depth])
// Update last node
if (propertyNode != null) {
lastNode = propertyNode
depth++
}
// Exit when exiting the current configuration object
if (propertyNode == null || depth >= properties.length) {
break
}
}
// If the configuration file is defining the property we are after, update it.
if (depth == properties.length) {
// The last node identified is an object expression, so do the assignment
if (lastNode.type === 'ObjectExpression') {
this.configuration =
this.configuration.slice(0, lastNode.range[0]) +
JSON.stringify(propertyValue) +
this.configuration.slice(lastNode.range[1])
}
// A misc object was found in the configuration file (e.g. an array, a string, a boolean,
// a number, etc.), just replace the whole range by our declaration
else {
this.configuration =
this.configuration.slice(0, lastNode.range[0]) +
JSON.stringify(propertyValue) +
this.configuration.slice(lastNode.range[1])
}
}
// Create nested properties in the configuration file
else {
// Build the declaration to inject
const declaration = this.getPropertyDeclaration(
properties,
depth,
propertyValue
)
// The last node identified is an object expression, so do the assignment
if (lastNode.type === 'ObjectExpression') {
// The object is blank (no properties) so replace the whole range by a new object containing the declaration
if (lastNode.properties.length === 0) {
this.configuration =
this.configuration.slice(0, lastNode.range[0]) +
'{' +
declaration +
'}' +
this.configuration.slice(lastNode.range[1])
}
// The object contains other properties, prepend our new one at the beginning
else {
this.configuration =
this.configuration.slice(0, lastNode.properties[0].range[0]) +
declaration +
',' +
this.configuration.slice(lastNode.properties[0].range[0])
}
}
// A misc object was found in the configuration file (e.g. an array, a string, a boolean,
// a number, etc.), just replace the whole range by our declaration
else {
this.configuration =
this.configuration.slice(0, lastNode.range[0]) +
'{' +
declaration +
'}' +
this.configuration.slice(lastNode.range[1])
}
}
// Logging
core.info(`Injection successful, new configuration:`)
core.info(this.configuration)
// Finally write the new configuration in the file
fs.writeFileSync(this.configurationFile, this.configuration, {
encoding: 'utf8'
})
}
} }
module.exports = {ConfigParser} module.exports = {ConfigParser}

View File

@@ -1,83 +1,156 @@
const { ConfigParser } = require('./config-parser') const fs = require('fs')
const fs = require("fs")
const assert = require('assert')
const srcFolder = `${process.cwd()}/src/fixtures` const {ConfigParser} = require('./config-parser')
const tmpFolder = `${process.cwd()}/src/fixtures/tmp` const {getTempFolder, compareFiles} = require('./test-helpers')
const expectedFolder = `${process.cwd()}/src/fixtures/expected`
const repoPath = "/amazing-new-repo/" // Get the temp folder
const tempFolder = getTempFolder()
// Cases to test
const cases = [ const cases = [
["next.config.js", { //
filePath: `${tmpFolder}/next.config.js`, // Default export
type: "next", //
pathName: "basePath",
newPath: repoPath
}],
["next.config.old.js", {
filePath: `${tmpFolder}/next.config.old.js`,
type: "next",
pathName: "basePath",
newPath: repoPath
}],
["next.config.old.missing.js", {
filePath: `${tmpFolder}/next.config.old.missing.js`,
type: "next",
pathName: "basePath",
newPath: repoPath
}],
["gatsby-config.js", {
filePath: `${tmpFolder}/gatsby-config.js`,
type: "gatsby",
pathName: "pathPrefix",
newPath: repoPath
}],
["gatsby-config.old.js", {
filePath: `${tmpFolder}/gatsby-config.old.js`,
type: "gatsby",
pathName: "pathPrefix",
newPath: repoPath
}],
["nuxt.config.js", {
filePath:`${tmpFolder}/nuxt.config.js`,
type: "nuxt",
pathName: "router",
subPathName: "base",
newPath: repoPath
}],
["nuxt.config.missing.js", {
filePath:`${tmpFolder}/nuxt.config.missing.js`,
type: "nuxt",
pathName: "router",
subPathName: "base",
newPath: repoPath
}],
["nuxt.config.old.js", {
filePath:`${tmpFolder}/nuxt.config.old.js`,
type: "nuxt",
pathName: "router",
subPathName: "base",
newPath: repoPath
}],
];
describe('configParser', () => { {
test.each(cases)( property: 'property',
"%p parsed correctly", source: `export default {}`,
(fileName, configuration) => { expected: `export default { property: "value" }`
srcFileName = `${srcFolder}/${fileName}` },
tmpFileName = `${tmpFolder}/${fileName}` {
expectedFileName = `${expectedFolder}/${fileName}` property: 'property',
fs.mkdirSync(tmpFolder, {recursive: true}) source: `export default { property: 0 }`, // property exists and is a number
fs.copyFileSync(srcFileName, tmpFileName) expected: `export default { property: "value" }`
const parser = new ConfigParser(configuration) },
parser.parse() {
property: 'property',
source: `export default { property: false }`, // property exists and is a boolean
expected: `export default { property: "value" }`
},
{
property: 'property',
source: `export default { property: "test" }`, // property exists and is a string
expected: `export default { property: "value" }`
},
{
property: 'property',
source: `export default { property: [1,2] }`, // property exists and is an array
expected: `export default { property: "value" }`
},
{
property: 'property',
source: `export default { property: null }`, // property exists and is null
expected: `export default { property: "value" }`
},
{
property: 'property',
source: `export default { property: {}}`, // property exists and is an object
expected: `export default { property: "value" }`
},
var expectedContent = fs.readFileSync(expectedFileName).toString() // Deep properties (injection 1)
var actualContent = fs.readFileSync(tmpFileName).toString() {
assert.equal(actualContent, expectedContent) property: 'property.b.c',
fs.rmSync(tmpFileName) source: `export default {}`,
} expected: `export default { property: { b: { c: "value" }}}`
) },
{
property: 'property.b.c',
source: `export default { property: 0 }`, // property exists and is a number
expected: `export default { property: { b: { c: "value" }}}`
},
{
property: 'property.b.c',
source: `export default { property: {}}`, // property exists and is an object
expected: `export default { property: { b: { c: "value" }}}`
},
// Deep properties (injection 2)
{
property: 'property.b.c',
source: `export default { property: { b: 0 }}`, // property exists and is a number
expected: `export default { property: { b: { c: "value" }}}`
},
{
property: 'property.b.c',
source: `export default { property: { b: {}}}`, // property exists and is an object
expected: `export default { property: { b: { c: "value" }}}`
},
{
property: 'property.b.c',
source: `export default { property: { b: { hello: 123}}}`, // property exists and is a non-empty object
expected: `export default { property: { b: { c: "value", hello: 123 }}}`
},
// Deep properties (existing properties)
{
property: 'a1.a2',
source: `export default { a2: false, a1: { a3: [12]}}`, // property exists and is a non-empty object
expected: `export default { a2: false, a1: { a2: "value", a3: [12]}}`
},
//
// Direct module exports
//
{
property: 'property',
source: `module.exports = {}`,
expected: `module.exports = { property: "value"}`
},
{
property: 'property',
source: `module.exports = { p1: 0}`,
expected: `module.exports = { property: "value", p1: 0}`
},
{
property: 'a.b.c',
source: `module.exports = { p1: 0}`,
expected: `module.exports = { a: { b: { c: "value" }}, p1: 0}`
},
//
// Indirect module exports
//
{
property: 'property',
source: `const config = {}; module.exports = config`,
expected: `const config = { property: "value"}; module.exports = config`
},
{
property: 'property',
source: `var config = {}; module.exports = config`,
expected: `var config = { property: "value"}; module.exports = config`
},
{
property: 'a.b.c',
source: `var config = {}; module.exports = config`,
expected: `var config = { a: { b: { c: "value"}}}; module.exports = config`
},
{
property: 'a.b.c',
source: `var config = { a: { b: [], c: "hello"}}; module.exports = config`,
expected: `var config = { a: { b: { c: "value"}, c: "hello"}}; module.exports = config`
}
]
describe('config-parser', () => {
cases.forEach(({property, source, expected}, index) => {
it(`Inject path properly for case #${index}`, () => {
// Write the source file
const sourceFile = `${tempFolder}/source.js`
fs.writeFileSync(sourceFile, source, {encoding: 'utf8'})
// Write the expected file
const expectedFile = `${tempFolder}/expected.js`
fs.writeFileSync(expectedFile, expected, {encoding: 'utf8'})
// Update the settings and do the injection
new ConfigParser({
configurationFile: sourceFile
}).inject(property, 'value')
// Compare the files
compareFiles(sourceFile, expectedFile)
})
})
}) })

View File

@@ -5,11 +5,13 @@ function getRequiredVars() {
return { return {
repositoryNwo: process.env.GITHUB_REPOSITORY, repositoryNwo: process.env.GITHUB_REPOSITORY,
githubToken: core.getInput('token'), githubToken: core.getInput('token'),
staticSiteGenerator: core.getInput('static_site_generator') staticSiteGenerator: core.getInput('static_site_generator'),
enablement: core.getInput('enablement') !== 'false'
} }
} }
module.exports = function getContext() { // Return the context object
function getContext() {
const requiredVars = getRequiredVars() const requiredVars = getRequiredVars()
for (const variable in requiredVars) { for (const variable in requiredVars) {
if (requiredVars[variable] === undefined) { if (requiredVars[variable] === undefined) {
@@ -19,3 +21,5 @@ module.exports = function getContext() {
core.debug('all variables are set') core.debug('all variables are set')
return requiredVars return requiredVars
} }
module.exports = {getContext}

View File

@@ -1,31 +0,0 @@
const core = require('@actions/core')
const axios = require('axios')
async function enablePages({ repositoryNwo, githubToken }) {
const pagesEndpoint = `https://api.github.com/repos/${repositoryNwo}/pages`
try {
const response = await axios.post(
pagesEndpoint,
{ build_type: 'workflow' },
{
headers: {
Accept: 'application/vnd.github.v3+json',
Authorization: `Bearer ${githubToken}`,
'Content-type': 'application/json',
},
}
)
core.info('Created pages site')
} catch (error) {
if (error.response && error.response.status === 409) {
core.info('Pages site exists')
return
}
core.error('Couldn\'t create pages site', error)
throw error
}
}
module.exports = enablePages

View File

@@ -1,48 +0,0 @@
const core = require('@actions/core')
const axios = require('axios')
const enablePages = require('./enable-pages')
describe('enablePages', () => {
const GITHUB_REPOSITORY = 'paper-spa/is-awesome'
const GITHUB_TOKEN = 'gha-token'
beforeEach(() => {
jest.restoreAllMocks()
// Mock error/warning/info/debug
jest.spyOn(core, 'error').mockImplementation(jest.fn())
jest.spyOn(core, 'warning').mockImplementation(jest.fn())
jest.spyOn(core, 'info').mockImplementation(jest.fn())
jest.spyOn(core, 'debug').mockImplementation(jest.fn())
})
it('makes a request to create a page', async () => {
jest
.spyOn(axios, 'post')
.mockImplementationOnce(() => Promise.resolve({ }))
await enablePages({ repositoryNwo: GITHUB_REPOSITORY, githubToken: GITHUB_TOKEN })
})
it('handles a 409 response when the page already exists', async () => {
jest
.spyOn(axios, 'post')
.mockImplementationOnce(() => Promise.reject({ response: { status: 409 } }))
// Simply assert that no error is raised
await enablePages({ repositoryNwo: GITHUB_REPOSITORY, githubToken: GITHUB_TOKEN })
})
it('re-raises errors on failure status codes', async () => {
jest
.spyOn(axios, 'post')
.mockImplementationOnce(() => Promise.reject({ response: { status: 404 } }))
try {
await enablePages({ repositoryNwo: GITHUB_REPOSITORY, githubToken: GITHUB_TOKEN })
} catch (error) {
expect(error.response.status).toEqual(404)
}
})
})

View File

@@ -1,8 +0,0 @@
import { resolve } from 'path'
export default {
alias: {
'style': resolve(__dirname, './assets/style')
},
pathPrefix: '/amazing-new-repo/',/* test */
}

View File

@@ -1,3 +0,0 @@
module.exports = {
pathPrefix: '/amazing-new-repo/'
}

View File

@@ -1,9 +0,0 @@
import { resolve } from 'path'
export default {
alias: {
'style': resolve(__dirname, './assets/style')
},
basePath: '/amazing-new-repo/'/* test */,
}

View File

@@ -1,4 +0,0 @@
module.exports={
basePath: '/amazing-new-repo/'/* test */,
}

View File

@@ -1,4 +0,0 @@
module.exports={
basePath: '/amazing-new-repo/'
}

View File

@@ -1,11 +0,0 @@
import { resolve } from 'path'
export default {
alias: {
'style': resolve(__dirname, './assets/style')
},
target: 'static',
router: {
base: '/amazing-new-repo/'
}
}

View File

@@ -1,11 +0,0 @@
import { resolve } from 'path'
export default {
router: {
base: '/amazing-new-repo/'
},
alias: {
'style': resolve(__dirname, './assets/style')
},
target: 'static'
}

View File

@@ -1,10 +0,0 @@
module.exports={
alias: {
'style': resolve(__dirname, './assets/style')
},
target: 'static',
router: {
base: '/amazing-new-repo/'
}
}

View File

@@ -1,8 +0,0 @@
import { resolve } from 'path'
export default {
alias: {
'style': resolve(__dirname, './assets/style')
},
pathPrefix: '/prefix',/* test */
}

View File

@@ -1,3 +0,0 @@
module.exports = {
pathPrefix: '/prefix'
}

View File

@@ -0,0 +1,2 @@
// Default Pages configuration for Gatsby
module.exports = { pathPrefix: "/docs/" }

View File

@@ -0,0 +1 @@
// This file is not read by the test suite

View File

@@ -0,0 +1,8 @@
module.exports = {
pathPrefix: "/docs/",
siteMetadata: {
title: `My Gatsby Site`,
siteUrl: `https://www.yourdomain.tld`,
},
plugins: [],
}

View File

@@ -0,0 +1,7 @@
module.exports = {
siteMetadata: {
title: `My Gatsby Site`,
siteUrl: `https://www.yourdomain.tld`,
},
plugins: [],
}

View File

@@ -1,9 +0,0 @@
import { resolve } from 'path'
export default {
alias: {
'style': resolve(__dirname, './assets/style')
},
basePath: '/gh-pages-test'/* test */,
}

View File

@@ -1,4 +0,0 @@
module.exports={
basePath: '/gh-pages-test'/* test */,
}

View File

@@ -1,4 +0,0 @@
module.exports={
}

View File

@@ -0,0 +1,6 @@
// Default Pages configuration for Next
const nextConfig = {
experimental: {images: {unoptimized: true}},
basePath: '/docs'
}
module.exports = nextConfig

View File

@@ -0,0 +1 @@
// This file is not read by the test suite

View File

@@ -0,0 +1,9 @@
/** @type {import('next').NextConfig} */
const nextConfig = {
experimental: {images: {unoptimized: true}},
basePath: '/docs',
reactStrictMode: true,
swcMinify: true
}
module.exports = nextConfig

View File

@@ -0,0 +1,7 @@
/** @type {import('next').NextConfig} */
const nextConfig = {
reactStrictMode: true,
swcMinify: true,
}
module.exports = nextConfig

View File

@@ -1,11 +0,0 @@
import { resolve } from 'path'
export default {
alias: {
'style': resolve(__dirname, './assets/style')
},
target: 'static',
router: {
base: 'some/path'
}
}

View File

@@ -1,8 +0,0 @@
import { resolve } from 'path'
export default {
alias: {
'style': resolve(__dirname, './assets/style')
},
target: 'static'
}

View File

@@ -1,10 +0,0 @@
module.exports={
alias: {
'style': resolve(__dirname, './assets/style')
},
target: 'static',
router: {
base: 'some/path'
}
}

View File

@@ -0,0 +1,17 @@
const getAllDynamicRoute = async function() {
const routes = await (async () => {
return ['/posts/hello-world', '/posts/hello-again'];
})();
return routes;
};
export default {
target: 'static',
router: {base: '/docs/'},
mode: 'universal',
generate: {
async routes () {
return getAllDynamicRoute();
}
}
};

View File

@@ -0,0 +1,15 @@
const getAllDynamicRoute = async function() {
const routes = await (async () => {
return ['/posts/hello-world', '/posts/hello-again'];
})();
return routes;
};
export default {
mode: 'universal',
generate: {
async routes () {
return getAllDynamicRoute();
}
}
};

View File

@@ -0,0 +1,2 @@
// Default Pages configuration for Nuxt
export default {target: 'static', router: {base: '/docs/'}}

View File

@@ -0,0 +1 @@
// This file is not read by the test suite

View File

@@ -0,0 +1,46 @@
export default {
// Disable server-side rendering: https://go.nuxtjs.dev/ssr-mode
target: 'static',
router: { base: "/docs/" },
ssr: false,
// Global page headers: https://go.nuxtjs.dev/config-head
head: {
title: 'nuxt',
htmlAttrs: {
lang: 'en'
},
meta: [
{ charset: 'utf-8' },
{ name: 'viewport', content: 'width=device-width, initial-scale=1' },
{ hid: 'description', name: 'description', content: '' },
{ name: 'format-detection', content: 'telephone=no' }
],
link: [
{ rel: 'icon', type: 'image/x-icon', href: '/favicon.ico' }
]
},
// Global CSS: https://go.nuxtjs.dev/config-css
css: [
],
// Plugins to run before rendering page: https://go.nuxtjs.dev/config-plugins
plugins: [
],
// Auto import components: https://go.nuxtjs.dev/config-components
components: true,
// Modules for dev and build (recommended): https://go.nuxtjs.dev/config-modules
buildModules: [
],
// Modules: https://go.nuxtjs.dev/config-modules
modules: [
],
// Build Configuration: https://go.nuxtjs.dev/config-build
build: {
}
}

View File

@@ -0,0 +1,44 @@
export default {
// Disable server-side rendering: https://go.nuxtjs.dev/ssr-mode
ssr: false,
// Global page headers: https://go.nuxtjs.dev/config-head
head: {
title: 'nuxt',
htmlAttrs: {
lang: 'en'
},
meta: [
{ charset: 'utf-8' },
{ name: 'viewport', content: 'width=device-width, initial-scale=1' },
{ hid: 'description', name: 'description', content: '' },
{ name: 'format-detection', content: 'telephone=no' }
],
link: [
{ rel: 'icon', type: 'image/x-icon', href: '/favicon.ico' }
]
},
// Global CSS: https://go.nuxtjs.dev/config-css
css: [
],
// Plugins to run before rendering page: https://go.nuxtjs.dev/config-plugins
plugins: [
],
// Auto import components: https://go.nuxtjs.dev/config-components
components: true,
// Modules for dev and build (recommended): https://go.nuxtjs.dev/config-modules
buildModules: [
],
// Modules: https://go.nuxtjs.dev/config-modules
modules: [
],
// Build Configuration: https://go.nuxtjs.dev/config-build
build: {
}
}

View File

@@ -1,37 +0,0 @@
const core = require('@actions/core')
const axios = require('axios')
const setPagesPath = require('./set-pages-path')
async function getPagesBaseUrl({ repositoryNwo, githubToken, staticSiteGenerator}) {
try {
const pagesEndpoint = `https://api.github.com/repos/${repositoryNwo}/pages`
core.info(`Get the Base URL to the page with endpoint ${pagesEndpoint}`)
const response = await axios.get(
pagesEndpoint,
{
headers: {
Accept: 'application/vnd.github.v3+json',
Authorization: `Bearer ${githubToken}`
}
}
)
pageObject = response.data
core.info(JSON.stringify(pageObject))
const siteUrl = new URL(pageObject.html_url)
if ( staticSiteGenerator ) {
setPagesPath({staticSiteGenerator, path: siteUrl.pathname})
}
core.setOutput('base_url', siteUrl.href)
core.setOutput('origin', siteUrl.origin)
core.setOutput('host', siteUrl.host)
core.setOutput('base_path', siteUrl.pathname)
} catch (error) {
core.error('Get on the Page failed', error)
throw error
}
}
module.exports = getPagesBaseUrl

View File

@@ -1,22 +1,28 @@
const core = require('@actions/core') const core = require('@actions/core')
const enablePages = require('./enable-pages')
const getPagesBaseUrl = require('./get-pages-base-url')
// All variables we need from the runtime are loaded here // All variables we need from the runtime are loaded here
const getContext = require('./context') const { getContext } = require('./context')
const { findOrCreatePagesSite } = require('./api-client')
const { setPagesPath } = require('./set-pages-path')
const outputPagesBaseUrl = require('./output-pages-base-url')
async function main() { async function main() {
try { try {
const context = getContext() const { repositoryNwo, githubToken, enablement, staticSiteGenerator } = getContext()
await enablePages(context)
await getPagesBaseUrl(context) const pageObject = await findOrCreatePagesSite({ repositoryNwo, githubToken, enablement })
const siteUrl = new URL(pageObject.html_url)
if (staticSiteGenerator) {
setPagesPath({ staticSiteGenerator, path: siteUrl.pathname })
}
outputPagesBaseUrl(siteUrl)
} catch (error) { } catch (error) {
core.setFailed(error) core.setFailed(error)
process.exit(1) process.exit(1)
} }
} }
// Main // Main
main() main()

View File

@@ -0,0 +1,10 @@
const core = require('@actions/core')
function outputPagesBaseUrl(siteUrl) {
core.setOutput('base_url', siteUrl.href)
core.setOutput('origin', siteUrl.origin)
core.setOutput('host', siteUrl.host)
core.setOutput('base_path', siteUrl.pathname)
}
module.exports = outputPagesBaseUrl

View File

@@ -1,17 +1,14 @@
const core = require('@actions/core') const core = require('@actions/core')
const axios = require('axios')
//const { expect, jest } = require('@jest/globals')
const getPagesBaseUrl = require('./get-pages-base-url') const outputPagesBaseUrl = require('./output-pages-base-url')
describe('getPagesBaseUrl', () => {
const GITHUB_REPOSITORY = 'paper-spa/is-awesome'
const GITHUB_TOKEN = 'gha-token'
describe('outputPagesBaseUrl', () => {
beforeEach(() => { beforeEach(() => {
jest.restoreAllMocks() jest.restoreAllMocks()
jest.spyOn(core, 'setOutput').mockImplementation((key, value) => { key, value }) jest.spyOn(core, 'setOutput').mockImplementation((key, value) => {
key, value
})
jest.spyOn(core, 'setFailed').mockImplementation(param => param) jest.spyOn(core, 'setFailed').mockImplementation(param => param)
// Mock error/warning/info/debug // Mock error/warning/info/debug
@@ -24,14 +21,13 @@ describe('getPagesBaseUrl', () => {
it('gets expected outputs for profile site', async () => { it('gets expected outputs for profile site', async () => {
const baseUrl = 'https://octocat.github.io/' const baseUrl = 'https://octocat.github.io/'
jest outputPagesBaseUrl(new URL(baseUrl))
.spyOn(axios, 'get')
.mockImplementationOnce(() => Promise.resolve({ data: { html_url: baseUrl } }))
await getPagesBaseUrl({ repositoryNwo: GITHUB_REPOSITORY, githubToken: GITHUB_TOKEN })
expect(core.setOutput).toHaveBeenCalledWith('base_url', baseUrl) expect(core.setOutput).toHaveBeenCalledWith('base_url', baseUrl)
expect(core.setOutput).toHaveBeenCalledWith('origin', 'https://octocat.github.io') expect(core.setOutput).toHaveBeenCalledWith(
'origin',
'https://octocat.github.io'
)
expect(core.setOutput).toHaveBeenCalledWith('host', 'octocat.github.io') expect(core.setOutput).toHaveBeenCalledWith('host', 'octocat.github.io')
expect(core.setOutput).toHaveBeenCalledWith('base_path', '/') expect(core.setOutput).toHaveBeenCalledWith('base_path', '/')
}) })
@@ -39,14 +35,13 @@ describe('getPagesBaseUrl', () => {
it('gets expected outputs for project site', async () => { it('gets expected outputs for project site', async () => {
const baseUrl = 'https://octocat.github.io/my-repo/' const baseUrl = 'https://octocat.github.io/my-repo/'
jest outputPagesBaseUrl(new URL(baseUrl))
.spyOn(axios, 'get')
.mockImplementationOnce(() => Promise.resolve({ data: { html_url: baseUrl } }))
await getPagesBaseUrl({ repositoryNwo: GITHUB_REPOSITORY, githubToken: GITHUB_TOKEN })
expect(core.setOutput).toHaveBeenCalledWith('base_url', baseUrl) expect(core.setOutput).toHaveBeenCalledWith('base_url', baseUrl)
expect(core.setOutput).toHaveBeenCalledWith('origin', 'https://octocat.github.io') expect(core.setOutput).toHaveBeenCalledWith(
'origin',
'https://octocat.github.io'
)
expect(core.setOutput).toHaveBeenCalledWith('host', 'octocat.github.io') expect(core.setOutput).toHaveBeenCalledWith('host', 'octocat.github.io')
expect(core.setOutput).toHaveBeenCalledWith('base_path', '/my-repo/') expect(core.setOutput).toHaveBeenCalledWith('base_path', '/my-repo/')
}) })
@@ -54,14 +49,13 @@ describe('getPagesBaseUrl', () => {
it('gets expected outputs for site with custom domain name', async () => { it('gets expected outputs for site with custom domain name', async () => {
const baseUrl = 'https://www.example.com/' const baseUrl = 'https://www.example.com/'
jest outputPagesBaseUrl(new URL(baseUrl))
.spyOn(axios, 'get')
.mockImplementationOnce(() => Promise.resolve({ data: { html_url: baseUrl } }))
await getPagesBaseUrl({ repositoryNwo: GITHUB_REPOSITORY, githubToken: GITHUB_TOKEN })
expect(core.setOutput).toHaveBeenCalledWith('base_url', baseUrl) expect(core.setOutput).toHaveBeenCalledWith('base_url', baseUrl)
expect(core.setOutput).toHaveBeenCalledWith('origin', 'https://www.example.com') expect(core.setOutput).toHaveBeenCalledWith(
'origin',
'https://www.example.com'
)
expect(core.setOutput).toHaveBeenCalledWith('host', 'www.example.com') expect(core.setOutput).toHaveBeenCalledWith('host', 'www.example.com')
expect(core.setOutput).toHaveBeenCalledWith('base_path', '/') expect(core.setOutput).toHaveBeenCalledWith('base_path', '/')
}) })

View File

@@ -1,46 +1,68 @@
const core = require('@actions/core') const core = require('@actions/core')
const axios = require('axios') const {ConfigParser} = require('./config-parser')
const { ConfigParser } = require('./config-parser')
async function setPagesPath({staticSiteGenerator, path}) { // Return the settings to be passed to a {ConfigParser} for a given
try { // static site generator and a Pages path value to inject
switch(staticSiteGenerator) function getConfigParserSettings(staticSiteGenerator, path) {
{ switch (staticSiteGenerator) {
case 'nuxt': case 'nuxt':
var ssConfig = { return {
filePath:"./nuxt.config.js", configurationFile: './nuxt.config.js',
type: "nuxt", blankConfigurationFile: `${__dirname}/blank-configurations/nuxt.js`,
pathName: "router", properties: {
subPathName: "base", // Configure a base path on the router
newPath: path 'router.base': path,
}
break;
case 'next':
var ssConfig = {
filePath:"./next.config.js",
type: "next",
pathName: "basePath",
newPath: path
}
break;
case 'gatsby':
var ssConfig = {
filePath: "./gatsby-config.js",
type: "gatsby",
pathName: "pathPrefix",
newPath: path
}
break;
default:
throw "Unknown config type"
}
let configParser = new ConfigParser(ssConfig) // Set the target to static too
configParser.parse() // https://nuxtjs.org/docs/configuration-glossary/configuration-target/
target: 'static'
}
}
case 'next':
// Next does not want a trailing slash
if (path.endsWith('/')) {
path = path.slice(0, -1)
}
} catch (error) { return {
core.warning(`We were unable to determine how to inject the site metadata into your config. Generated URLs may be incorrect. The base URL for this site should be ${path}. Please ensure your framework is configured to generate relative links appropriately.`, error) configurationFile: './next.config.js',
blankConfigurationFile: `${__dirname}/blank-configurations/next.js`,
properties: {
// Configure a base path
basePath: path,
// Disable server side image optimization too
// https://nextjs.org/docs/api-reference/next/image#unoptimized
'experimental.images.unoptimized': true
}
}
case 'gatsby':
return {
configurationFile: './gatsby-config.js',
blankConfigurationFile: `${__dirname}/blank-configurations/gatsby.js`,
properties: {
// Configure a path prefix
pathPrefix: path
}
}
default:
throw `Unsupported static site generator: ${staticSiteGenerator}`
} }
} }
module.exports = setPagesPath // Inject Pages configuration in a given static site generator's configuration file
function setPagesPath({staticSiteGenerator, path}) {
try {
// Parse the configuration file and try to inject the Pages configuration in it
const settings = getConfigParserSettings(staticSiteGenerator, path)
new ConfigParser(settings).injectAll()
} catch (error) {
// Logging
core.warning(
`We were unable to determine how to inject the site metadata into your config. Generated URLs may be incorrect. The base URL for this site should be ${path}. Please ensure your framework is configured to generate relative links appropriately.`,
error
)
}
}
module.exports = {getConfigParserSettings, setPagesPath}

View File

@@ -0,0 +1,53 @@
const fs = require('fs')
const path = require('path')
const {getConfigParserSettings} = require('./set-pages-path')
const {ConfigParser} = require('./config-parser')
const {getTempFolder, compareFiles} = require('./test-helpers')
// Get the temp folder
const tempFolder = getTempFolder()
// Test suite
describe('configParser', () => {
// Iterate over the static site generators
;['next', 'nuxt', 'gatsby'].forEach(staticSiteGenerator => {
// Folder containing the fixtures for a given static site generator
const fixtureFolder = `${__dirname}/fixtures/${staticSiteGenerator}`
// Iterate over the fixtures
fs.readdirSync(fixtureFolder).forEach(configurationFile => {
// Ignore expectation
if (configurationFile.endsWith('.expected.js')) {
return
}
it(`Inject path properly for ${staticSiteGenerator} in ${configurationFile}`, async () => {
// Get settings for the static site generator
const settings = getConfigParserSettings(staticSiteGenerator, '/docs/')
// Copy the source fixture to a temp file
const fixtureSourceFile = `${fixtureFolder}/${configurationFile}`
const fixtureTargetFile = `${tempFolder}/${configurationFile}`
if (configurationFile != 'blank.js') {
fs.copyFileSync(fixtureSourceFile, fixtureTargetFile)
} else if (fs.existsSync(fixtureTargetFile)) {
fs.rmSync(fixtureTargetFile)
}
// Update the settings and do the injection
settings.configurationFile = fixtureTargetFile
new ConfigParser(settings).injectAll()
// Read the expected file
const expectedFile = `${fixtureFolder}/${path.basename(
configurationFile,
'.js'
)}.expected.js`
// Compare the actual and expected files
compareFiles(settings.configurationFile, expectedFile)
})
})
})
})

37
src/test-helpers.js Normal file
View File

@@ -0,0 +1,37 @@
const fs = require('fs')
const prettier = require('prettier')
const assert = require('assert')
// Create and return the path to a temp folder
function getTempFolder() {
const tmpFolder = `${__dirname}/fixtures/tmp`
if (!fs.existsSync(tmpFolder)) {
fs.mkdirSync(tmpFolder, {recursive: true})
}
return tmpFolder
}
// Read a JavaScript file and return it formatted
function formatFile(file) {
const fileContent = fs.readFileSync(file, 'utf8')
return prettier.format(fileContent, {
parser: 'espree',
// Prettier options
printWidth: 80,
tabWidth: 2,
useTabs: false,
semi: false,
singleQuote: true,
trailingComma: 'none',
bracketSpacing: false,
arrowParens: 'avoid'
}).trim()
}
// Compare two JavaScript files
function compareFiles(actualFile, expectedFile) {
assert.equal(formatFile(actualFile), formatFile(expectedFile))
}
module.exports = {getTempFolder, formatFile, compareFiles}