Skip to content

Commit

Permalink
build: update content when upstream changes
Browse files Browse the repository at this point in the history
This commit adds support to update the content when there is a change
upstream by:

- Adding a webhook handler to redirect `push` and `release` events from
  `electron/electron` to this repo.
- Adding a GitHub action that responds to `repository_dispatch` events
  comming from the previous webhook.

More information can be found in #19

- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -

Fix #19
  • Loading branch information
molant committed May 4, 2021
1 parent 1191485 commit 5845a72
Show file tree
Hide file tree
Showing 22 changed files with 3,050 additions and 66 deletions.
1 change: 1 addition & 0 deletions .env.example
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
GITHUB_TOKEN=
24 changes: 24 additions & 0 deletions .github/workflows/update-docs.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
name: 'Update docs'

on:
repository_dispatch:
types: [doc_changes]

jobs:
update-docs:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- uses: actions/setup-node@v2
with:
node-version: '14'
- name: Install dependencies
run: 'yarn'
- name: Update pinned version
run: 'yarn update-pinned-version ${{ github.event.client_payload.sha }}'
- name: 'Prebuild'
run: 'yarn prebuild'
- name: 'Create PR'
run: 'yarn process-docs-changes'
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
node_modules
.docusaurus
.DS_Store
.env
.vscode/settings.json
build/
content/
Expand Down
9 changes: 9 additions & 0 deletions .vscode/extensions.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
{
// See http://go.microsoft.com/fwlink/?LinkId=827846 to learn about workspace recommendations.
// Extension identifier format: ${publisher}.${name}. Example: vscode.csharp

// List of extensions which should be recommended for users of this workspace.
"recommendations": ["esbenp.prettier-vscode", "orta.vscode-jest"],
// List of extensions recommended by VS Code that should not be recommended for users of this workspace.
"unwantedRecommendations": []
}
11 changes: 11 additions & 0 deletions .vscode/launch.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,17 @@
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
"version": "0.2.0",
"configurations": [
{
"type": "node",
"name": "vscode-jest-tests",
"request": "launch",
"program": "${workspaceFolder}/node_modules/jest/bin/jest",
"args": ["--runInBand","--coverageProvider=v8"],
"cwd": "${workspaceFolder}",
"console": "integratedTerminal",
"internalConsoleOptions": "neverOpen",
"disableOptimisticBPs": true
},
{
"type": "pwa-node",
"request": "launch",
Expand Down
35 changes: 31 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -44,10 +44,37 @@ yarn start

`yarn start` starts a local development server and open up a browser window. Most changes are reflected live without having to restart the server.

## Build
# Repository content organization

```console
yarn build
This repository contains the code for 3 related things:

- The code to generate the contents of https://beta.electronjs.org
- [`create-electron-documentation`][ced] package
- The webhook that receives updates from `electron/electron` and
sends a `repository_dispatch` to trigger GitHub actions.

The content of this repository is organized as follows:

```
└─ root
|
├─ .github/workflows → The definitions for the GitHub actions
|
├─ api → The webhook server responsible of triggering
| GitHub actions
|
├─ create-electron-documentation → Code for the npm package
| of the same name. Read the readme in the folder
| for more information.
|
├─ scripts → The code for the package.json tasks and GitHub
| actions
|
├─ spec → Tests for the scripts
|
├─ src → Docusaurus code
|
├─ static → Docusaurus static assets
```

This command generates static content into the `build` directory and can be served using any static contents hosting service.
[ced]: https://npmjs.com/package/create-electron-documentation
2 changes: 2 additions & 0 deletions api/.env.example
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
GITHUB_TOKEN=
SECRET=
21 changes: 21 additions & 0 deletions api/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
//@ts-check
require('dotenv-safe').config();
const express = require('express');
const app = express();
const port = process.env.PORT || 3000;

const { addWebhooks } = require('./routes/webhook');

const start = async ()=>{
await addWebhooks(app);

app.get('/', (req, res) => {
res.send(`There's nothing here!`);
});

app.listen(port, () => {
console.log(`API listening on port ${port}`);
});
};

start();
23 changes: 23 additions & 0 deletions api/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
{
"name": "webhook",
"version": "1.0.0",
"description": "",
"main": "index.js",
"scripts": {
"start": "node index.js",
"test": "echo \"Error: no test specified\" && exit 1"
},
"keywords": [],
"author": "",
"license": "ISC",
"dependencies": {
"dotenv-safe": "^8.2.0",
"express": "^4.17.1",
"got": "^11.8.2",
"latest-version": "^5.1.0",
"semver": "^6.3.0"
},
"devDependencies": {
"@octokit/webhooks-types": "^3.71.2"
}
}
153 changes: 153 additions & 0 deletions api/routes/webhook.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,153 @@
//@ts-check

const semver = require('semver');
const got = require('got').default;

const {
getLatestInformation,
isEvent,
verifyIntegrity,
} = require('../utils/utils');

const SOURCE_REPO = 'electron/electron';
const TARGET_REPO = 'electron/electronjs.org-new';
const GITHUB_TOKEN = process.env.GITHUB_TOKEN;
const DOC_CHANGES_TYPE = 'doc_changes';

/**
* @typedef CustomRepositoryDispatchPayload
* @property {string} sha
*/

/**
* Sends a `repository_dispatch` event top the given repo `target`
* with the type `doc_changes` and the given commit `sha` as part
* of the payload.
* @param {string} target The repo to send the event to
* @param {string} sha The commit's SHA
*/
const sendRepositoryDispatchEvent = async (target, sha) => {
return got.post(`https://github.com/${target}/dispatches`, {
headers: {
Authorization: `Token ${GITHUB_TOKEN}`,
Accept: 'application/vnd.github.v3+json',
'Content-Type': 'application/json',
},
body: {
event_type: DOC_CHANGES_TYPE, // This is the only event we can send for now
client_payload: {
sha,
},
},
});
};

/**
* Verifies there is at least one file added, modified, or removed
* in the given `folder` through all the commits associated in the
* push.
* @param {import('@octokit/webhooks-types').PushEvent} pushEvent
* @param {string} folder
*/
const areFilesInFolderChanged = (pushEvent, folder) => {
const isInPath = (file) => {
return file.includes(folder);
};

const { commits } = pushEvent;

return commits.some((commit) => {
return (
commit.modified.some(isInPath) ||
commit.added.some(isInPath) ||
commit.removed.some(isInPath)
);
});
};

/**
* Handler for the GitHub webhook `push` event.
* @param {import('express').Request} req
* @param {import('express').Response} res
*/
const pushHandler = async (req, res) => {
const { branch } = await getLatestInformation();
const ref = `refs/heads/${branch}`;

/** @type {import('@octokit/webhooks-types').PushEvent} */
const payload = JSON.parse(req.body);

if (
payload.ref === ref &&
payload.repository.full_name === SOURCE_REPO &&
areFilesInFolderChanged(payload, 'docs')
) {
console.log('Send notification');

await sendRepositoryDispatchEvent(TARGET_REPO, payload.after);

return res.status(200).send('Handled');
}

return res.status(200).send('Unhandled');
};

/**
* Handler for the GitHub webhook `release` event.
* The payload will be processed only if the release
* payload is for the stable branch.
* @param {import('express').Request} req
* @param {import('express').Response} res
*/
const releaseHandler = async (req, res) => {
/** @type {import('@octokit/webhooks-types').ReleaseEvent} */
const payload = JSON.parse(req.body);

const { release } = await getLatestInformation();

// Tags can be v14.0.0-nightly.20210504, v13.0.0-beta.21, v10.4.5, etc.
// We only want to process the stable ones, i.e.: v10.4.5
// so we remove the initial `v` and we "clean it". If the cleaned
// string is the same as before, then it's a stable release.
// We also check that the new release is greater or equal than the
// published npm version. There can be 30-120s delay between a GitHub
// release and an npm one.
const tag = payload.release.tag_name.replace(/^v/, '');
const isStable = semver.clean(tag) === tag;

if (
payload.action === 'released' &&
!payload.release.draft &&
!payload.release.prerelease &&
isStable &&
semver.gte(tag, release)
) {
await sendRepositoryDispatchEvent(
TARGET_REPO,
payload.release.target_commitish
);

return res.status(200).send('Handled');
} else {
return res.status(200).send('Undhandled');
}
};

/**
* Adds the right handles for the `push` and `release`
* webhooks to the given `app`.
* @param {import('express').Application} app
*/
const addWebhooks = async (app) => {
app.post('/webhook/push', isEvent('push'), verifyIntegrity, pushHandler);
app.post(
'/webhook/release',
isEvent('release'),
verifyIntegrity,
releaseHandler
);
};

module.exports = {
addWebhooks,
};
81 changes: 81 additions & 0 deletions api/utils/utils.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
const crypto = require('crypto');
const latestVersion = require('latest-version');

const SECRET = process.env.SECRET;

let _stableVersion = '';
let _stableBranch = '';
let _lastUpdated = 0;
const CACHE_TIMEOUT = 5 * 60 * 1000; // we cache version for 5 minutes

const getLatestInformation = async () => {
const now = Date.now();
if (now - _lastUpdated > CACHE_TIMEOUT) {
_stableVersion = await latestVersion('electron');
_stableBranch = _stableVersion.replace(/\.d+\.\d+$/, '-x-y');
}

return {
version: _stableVersion,
branch: _stableBranch,
};
};

/**
* Middleware to validate the handler is the right one
* for the received event.
* @param {string} event
* @returns {import('express').Handler}
*/
const isEvent = (event) => {
return (req, res, next) => {
if (req.header('X-GitHub-Event') !== event) {
return res.status(400).send();
}

return next();
};
};

/**
* Middleware to verify the integraty of a GitHub webhook
* by using the `X-Hub-Signature`, the secret and the payload.
* @param {import('express').Request} req
* @param {import('express').Response} res
* @param {import('express').NextFunction} next
*/
const verifyIntegrity = (req, res, next) => {
if (!req.header('X-Hub-Signature')) {
console.error(`Missing singature in payload`);
return res.status(400).send(`Missing singature in payload`);
}

try {
const signature = Buffer.from(req.header('X-Hub-Signature'));
const payload = req.body;

const signedPayload = Buffer.from(
`sha1=${crypto.createHmac('sha1', SECRET).update(payload).digest('hex')}`
);

if (signature.length !== signedPayload.length) {
console.error(`Invalid signature`);
return res.status(400).send(`Invalid signature`);
}

if (!crypto.timingSafeEqual(signature, signedPayload)) {
console.error(`Invalid signature`);
return res.status(400).send(`Invalid signature`);
}
next();
} catch (e) {
console.error(`Invalid signature`);
return res.status(400).send(`Invalid signature`);
}
};

module.exports = {
isEvent,
verifyIntegrity,
getLatestInformation,
};
Loading

0 comments on commit 5845a72

Please sign in to comment.