The way versioned documents are supported on the site differs a bit from
Docusaurus' documentation:

- Each doc change to a branch is build and publish separately.
- The links to the versions are `_blank` to bypass `react-router` even
  though the content is in the same server. The reason is that because
  it is built at different steps it is not "found".

This is achieve thanks to `electron-website-updater` sending a
`repository_dispatch` event with 2 different types of actions for each
`push` event that happens in `electron/electron` that contains doc
changes. The 2 types of events are:

* `doc_changes_branches`: Updates to documentation branches (does not
  matter if they are the latest or not).
* `doc_changes`: Updates to the latest stable branch.

The tasks to perform are almost identical and are split in two different
GitHub actions for each event:

1. Download the markdown for the SHA, check if there are new version
   branches and update `versions-info.json`, update the SHA, and publish
   those changes to Git.
   This is done in `update-docs-XXX.yml` when we receive the event.
2. Build the content for that branch and publish in the right place.
   This is done in `push-XXX.yml` when there is a push to `main` or a
   `vXX` branch.

The deployment is done to the storage service. The difference is that a
branch will only push `assets` and `docs` while `main` will publish
everything.

Additionally, Crowdin gets updated every time there is a change in
`main` to make sure the latest content is always uploaded.

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

Fix #118
This commit is contained in:
Antón Molleda 2021-10-13 20:16:09 -07:00
Родитель 1f37587ca5
Коммит 17a1bd6872
16 изменённых файлов: 385 добавлений и 62 удалений

7
.github/workflows/push-main.yml поставляемый
Просмотреть файл

@ -11,15 +11,18 @@ jobs:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- name: Use Node.js 14
uses: actions/setup-node@v2
with:
node-version: 14
- name: Install dependencies
uses: bahmutov/npm-install@HEAD
- name: Upload sources to Crowdin
run: 'yarn i18n:upload'
env:
CROWDIN_PERSONAL_TOKEN: ${{ secrets.CROWDIN_PERSONAL_TOKEN }}
- name: Test
run: yarn test
env:

38
.github/workflows/push-others.yml поставляемый Normal file
Просмотреть файл

@ -0,0 +1,38 @@
name: Push on version branch
on:
push:
branches:
- 'v**'
jobs:
tests:
name: Push updates to previous versions
runs-on: ubuntu-latest
steps:
- name: Set environment variables
run: |
echo "GIT_BRANCH=${GITHUB_REF##*/}" >> $GITHUB_ENV
- uses: actions/checkout@v2
- name: Use Node.js 14
uses: actions/setup-node@v2
with:
node-version: 14
- name: Install dependencies
uses: bahmutov/npm-install@HEAD
- name: 'Build branch version (EN)'
run: |
node scripts/build-as-doc-version.js ${GIT_BRANCH}
yarn i18n:build en
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- name: 'Publish Assets to Storage'
run: ./scripts/bin/azcopy copy "./build/assets/*" "https://electronjsorg.blob.core.windows.net/%24web/assets?$SAS" --recursive
env:
SAS: ${{ secrets.SAS }}
- name: 'Publish docs to Storage'
run: ./scripts/bin/azcopy copy "./build/docs/*" "https://electronjsorg.blob.core.windows.net/%24web/docs?$SAS" --recursive
env:
SAS: ${{ secrets.SAS }}

25
.github/workflows/update-docs-branch.yml поставляемый Normal file
Просмотреть файл

@ -0,0 +1,25 @@
name: 'Update docs'
on:
repository_dispatch:
types: [doc_changes_branches]
jobs:
update-docs:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- uses: actions/setup-node@v2
with:
node-version: '14'
- name: 'Switch branches'
# We switch to the version branch or create a new one if needed
run: git fetch origin && git checkout -t origin/v${{ github.event.client_payload.branch}} || git checkout -b v${{ github.event.client_payload.branch}}
- name: Install dependencies
run: 'yarn'
- name: 'Prebuild'
run: 'yarn pre-build ${{ github.event.client_payload.sha }}'
- name: 'Push changes or create PR'
run: 'yarn process-docs-changes'
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

12
.github/workflows/update-docs.yml поставляемый
Просмотреть файл

@ -14,15 +14,9 @@ jobs:
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 pre-build'
- name: Upload sources to Crowdin
run: yarn i18n:upload
env:
CROWDIN_PERSONAL_TOKEN: ${{ secrets.CROWDIN_PERSONAL_TOKEN }}
- name: 'Create PR'
- name: 'Download docs'
run: 'yarn pre-build ${{ github.event.client_payload.sha }}'
- name: 'Push changes or create PR'
run: 'yarn process-docs-changes'
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

5
.gitignore поставляемый
Просмотреть файл

@ -3,7 +3,10 @@ node_modules
.DS_Store
.env
.vscode/settings.json
.tmp/
build/
content/
i18n/
!i18n/en/
!i18n/en/
docs/
!docs/latest

Просмотреть файл

@ -2,13 +2,14 @@
const npm2yarn = require('@docusaurus/remark-plugin-npm2yarn');
const fiddleEmbedder = require('./src/transformers/fiddle-embedder.js');
const apiLabels = require('./src/transformers/api-labels.js');
const docVersions = require('./versions-info.json');
module.exports = {
title: 'Electron',
tagline: 'Build cross-platform desktop apps with JavaScript, HTML, and CSS',
url: 'https://electronjs.org',
baseUrl: '/',
onBrokenLinks: 'throw',
onBrokenLinks: 'warn',
onBrokenMarkdownLinks: 'warn',
favicon: 'assets/img/favicon.ico',
organizationName: 'electron',
@ -73,6 +74,12 @@ module.exports = {
label: 'Releases',
position: 'right',
},
{
type: 'dropdown',
label: 'View another version',
position: 'right',
items: docVersions,
},
{
type: 'localeDropdown',
position: 'right',

Просмотреть файл

@ -26,5 +26,13 @@
"item.label.GitHub": {
"message": "GitHub",
"description": "Navbar item with label GitHub"
},
"item.label.View another version": {
"message": "View another version",
"description": "Navbar item with label View another version"
},
"item.label.Current": {
"message": "Current",
"description": "Navbar item with label Current"
}
}

Просмотреть файл

@ -16,17 +16,9 @@ describe('process-docs-changes', () => {
expect(gitMock.pushChanges).toHaveBeenCalledTimes(0);
});
it('does not create any PR if package.json is not modified', async () => {
gitMock.getChanges.mockResolvedValue('M sidebars.json');
await processDocsChanges();
expect(gitMock.createPR).toHaveBeenCalledTimes(0);
expect(gitMock.pushChanges).toHaveBeenCalledTimes(0);
});
it('pushes changes directly to main if only package.json is modified', async () => {
gitMock.getChanges.mockResolvedValue('M package.json');
gitMock.getCurrentBranchName.mockResolvedValue('main');
await processDocsChanges();
@ -40,10 +32,12 @@ describe('process-docs-changes', () => {
);
});
it('does create a PR if more files than package.json are modified', async () => {
it('does create a PR if more files than package.json are modified and branch is tracked', async () => {
gitMock.getChanges.mockResolvedValue(
'M package.json\nM sidebars.json\nU randomDoc.md'
);
gitMock.isCurrentBranchTracked.mockResolvedValue(true);
gitMock.getCurrentBranchName.mockResolvedValue('main');
await processDocsChanges();
@ -57,4 +51,24 @@ describe('process-docs-changes', () => {
'"chore: update ref to docs (🤖)"'
);
});
it('does push changes if branch is not tracked regardless of the number of files', async () => {
gitMock.getChanges.mockResolvedValue('M package.json\n');
gitMock.isCurrentBranchTracked.mockResolvedValue(false);
await processDocsChanges();
expect(gitMock.pushChanges).toHaveBeenCalledTimes(1);
expect(gitMock.createPR).toHaveBeenCalledTimes(0);
});
it('does push changes if branch is tracked and no new files are added', async () => {
gitMock.getChanges.mockResolvedValue('M package.json\nM randomdoc.md');
gitMock.isCurrentBranchTracked.mockResolvedValue(false);
await processDocsChanges();
expect(gitMock.pushChanges).toHaveBeenCalledTimes(1);
expect(gitMock.createPR).toHaveBeenCalledTimes(0);
});
});

Просмотреть файл

@ -0,0 +1,71 @@
/**
* This script takes a version passed as a parameter (i.e. v15-x-y), the
* current contents available under `/docs/latest`, and builds the project
* in such a way that the documentation is made available under
* `/docs/v15-x-y` or equivalent.
*/
//@ts-check
const fs = require('fs-extra');
const globby = require('globby');
/**
*
* @param {string} version
*/
const moveDocs = async (version) => {
await fs.move('docs/latest', `docs/${version}`);
const files = await globby([`docs/${version}/**/*.md`]);
for (const file of files) {
const content = await fs.readFile(file, 'utf-8');
let updatedContent = content.replace(/docs\/latest/gm, `docs/${version}`);
updatedContent = content.replace(/latest\//gm, `${version}/`);
await fs.writeFile(file, updatedContent, 'utf-8');
}
};
/**
*
* @param {string} version
*/
const updateConfigFiles = async (version) => {
const configFiles = ['docusaurus.config.js', 'sidebars.js'];
for (const configFile of configFiles) {
const content = await fs.readFile(configFile, 'utf-8');
const updatedContent = content.replace(/latest/g, version);
await fs.writeFile(configFile, updatedContent, 'utf-8');
}
};
/**
*
* @param {string} version
*/
const publishAsVersion = async (version) => {
await moveDocs(version);
await updateConfigFiles(version);
};
// When a file is run directly from Node.js, `require.main` is set to its module.
// That means that it is possible to determine whether a file has been run directly
// by testing `require.main === module`.
// https://nodejs.org/docs/latest/api/modules.html#modules_accessing_the_main_module
if (require.main === module) {
const version = process.argv[2];
if (!version) {
console.error('Please provide a version');
} else if (!version.match(/v\d+/)) {
console.error('Version should be like "v12"');
} else {
publishAsVersion(version);
}
}
module.exports = {
publishAsVersion,
};

Просмотреть файл

@ -8,15 +8,12 @@ const {
const updateConfig = async (locale) => {
const baseUrl = locale !== defaultLocale ? `/${locale}/` : '/';
// Translations might not be completely in sync and we need to keep publishing
const onBrokenLinks = locale !== defaultLocale ? `warn` : `throw`;
const configPath = join(__dirname, '../docusaurus.config.js');
let docusaurusConfig = await fs.readFile(configPath, 'utf-8');
docusaurusConfig = docusaurusConfig
.replace(/baseUrl: '.*?',/, `baseUrl: '${baseUrl}',`)
.replace(/onBrokenLinks: '.*?',/, `onBrokenLinks: '${onBrokenLinks}',`);
.replace(/baseUrl: '.*?',/, `baseUrl: '${baseUrl}',`);
await fs.writeFile(configPath, docusaurusConfig, 'utf-8');
};

Просмотреть файл

@ -16,6 +16,7 @@ const { addFrontmatter } = require('./tasks/add-frontmatter');
const { createSidebar } = require('./tasks/create-sidebar');
const { fixContent } = require('./tasks/md-fixers');
const { copyNewContent } = require('./tasks/copy-new-content');
const { updateVersionsInfo } = require('./tasks/update-versions-info');
const { sha } = require('../package.json');
const DOCS_FOLDER = path.join('docs', 'latest');
@ -84,6 +85,9 @@ const start = async (source) => {
console.log('Updating sidebar.js');
await createSidebar('docs', path.join(process.cwd(), 'sidebars.js'));
console.log('Updating docs versions');
await updateVersionsInfo();
};
start(process.argv[2]);

Просмотреть файл

@ -1,7 +1,7 @@
/**
* Checks if there are any changes in the repo and creates or updates
* a PR if needed. This is part of the `update-docs.yml` workflow and
* depends on `update-pinned-version` and `prebuild` being run before
* depends on `update-pinned-version` and `pre-build` being run before
* in order to produce the right result.
*/
@ -14,8 +14,13 @@ if (
process.exit(1);
}
const { execute } = require('./utils/execute');
const { createPR, getChanges, pushChanges } = require('./utils/git-commands');
const {
createPR,
getChanges,
pushChanges,
isCurrentBranchTracked,
getCurrentBranchName,
} = require('./utils/git-commands');
const HEAD = 'main';
const PR_BRANCH = 'chore/docs-updates';
@ -24,20 +29,7 @@ const EMAIL = 'electron@github.com';
const NAME = 'electron-bot';
/**
* Wraps a function on a try/catch and changes the exit code if it fails.
* @param {Function} func
*/
const changeExitCodeIfException = async (func) => {
try {
await func();
} catch (e) {
console.error(e);
process.exitCode = 1;
}
};
/**
* Checks if there are new document files by parsing the given
* Checks if there are new document files (*.md) by parsing the given
* `git status --porcelain` input.
* This is done by looking at the status of each file:
* - `A` means it is new and has been staged
@ -49,35 +41,49 @@ const newDocFiles = (gitOutput) => {
const lines = gitOutput.split('\n');
const newFiles = lines.filter((line) => {
const trimmedLine = line.trim();
return trimmedLine.startsWith('U') || trimmedLine.startsWith('??');
return (
trimmedLine.endsWith('.md') &&
(trimmedLine.startsWith('U') || trimmedLine.startsWith('??'))
);
});
return newFiles;
};
/**
* Analyzes the current `git status` of the local repo and branch to
* see if there are new files or just modifications to existing ones.
*
* - If there is just modifications it pushes the changes directly to
* the branch upstream.
* - If there is new content it creates a new branch and opens a PR for
* review. The format of the pr branch name is `chore/docs-updates` for `main`
* and `chore/docs-updates-vXX-Y-X` for the ones targetting `vXX-Y-X`.
* - Creates a new branch and pushes the changes directly if it does
* not exist.
*/
const processDocsChanges = async () => {
const output = await getChanges();
const branchIsTracked = await isCurrentBranchTracked();
const branchName = await getCurrentBranchName();
if (output === '') {
console.log('Nothing updated, skipping');
return;
} else if (!/M\s+package\.json/.test(output)) {
console.log('package.json is not modified, skipping');
return;
} else {
console.log(`Uploading changes to Crowdin`);
await execute(`yarn i18n:upload`);
const newFiles = newDocFiles(output);
if (newFiles.length > 0) {
const prBranchName =
branchName === 'main' ? PR_BRANCH : `${PR_BRANCH}-${branchName}`;
if (newFiles.length > 0 && branchIsTracked) {
console.log(`New documents available:
${newFiles.join('\n')}`);
await createPR(PR_BRANCH, HEAD, EMAIL, NAME, COMMIT_MESSAGE);
await createPR(prBranchName, branchName, EMAIL, NAME, COMMIT_MESSAGE);
} else {
console.log(
`Only existing content has been modified. Pushing changes directly.`
);
await pushChanges(HEAD, EMAIL, NAME, COMMIT_MESSAGE);
await pushChanges(branchName, EMAIL, NAME, COMMIT_MESSAGE);
}
}
};
@ -87,7 +93,12 @@ ${newFiles.join('\n')}`);
// by testing `require.main === module`.
// https://nodejs.org/docs/latest/api/modules.html#modules_accessing_the_main_module
if (require.main === module) {
changeExitCodeIfException(processDocsChanges);
process.addListener('unhandledRejection', (e) => {
console.error(e);
process.exit(1);
});
processDocsChanges();
}
module.exports = {

Просмотреть файл

@ -0,0 +1,52 @@
//@ts-check
const fs = require('fs').promises;
const {
getRemoteBranches,
getCurrentBranchName,
} = require('../utils/git-commands');
const VERSIONS_INFO = 'versions-info.json';
const updateVersionsInfo = async () => {
const branches = await getRemoteBranches();
const versions = branches
.map((branch) => branch.split('/').pop())
.filter((branch) => /v\d+-x-y/.test(branch));
// We might be creating a new docs version branch
const current = await getCurrentBranchName();
if (!versions.includes(current)) {
versions.push(current);
}
const localVersions = JSON.parse(await fs.readFile(VERSIONS_INFO, 'utf-8'));
for (const version of versions) {
let exists = false;
for (const localVersion of localVersions) {
console.log(localVersion);
console.log(version);
exists = exists || localVersion.label === version;
}
if (!exists) {
console.log(`New version ${version} found`);
localVersions.push({
label: version,
href: `https://electronjs.org/docs/${version}`,
target: '_blank',
});
}
}
await fs.writeFile(
VERSIONS_INFO,
JSON.stringify(localVersions, null, 2),
'utf-8'
);
};
module.exports = {
updateVersionsInfo: updateVersionsInfo,
};

Просмотреть файл

@ -2,7 +2,7 @@
const executeMock = jest.createMockFromModule('../execute');
jest.mock('../execute', () => executeMock);
const octokitMock = {
pulls: { list: jest.fn(), create: jest.fn() },
pulls: { list: jest.fn(), create: jest.fn(), requestReviewers: jest.fn() },
};
const github = {
getOctokit: () => {

Просмотреть файл

@ -1,6 +1,7 @@
const github = require('@actions/github');
const { execute } = require('./execute');
const GITHUB_TOKEN = process.env.GITHUB_TOKEN;
const REVIEWERS = ['molant', 'erickzhao'];
/**
* Creates a new commit with the current changes.
@ -14,7 +15,7 @@ const createCommit = async (email, name, commitMessage) => {
await execute(`git config --global user.email ${email}`);
await execute(`git config --global user.name ${name}`);
await execute(`git add .`);
await execute(`git commit -am ${commitMessage}`);
await execute(`git commit -m ${commitMessage} --no-verify`);
};
/**
@ -27,7 +28,8 @@ const getChanges = async () => {
};
/**
* Creates a new commit and pushes the given branch
* Creates a new commit and pushes the given branch, creating it
* upstream if needed.
* @param {string} branch
* @param {string} email
* @param {string} name
@ -35,13 +37,21 @@ const getChanges = async () => {
*/
const pushChanges = async (branch, email, name, message) => {
await createCommit(email, name, message);
await execute(`git pull --rebase`);
await execute(`git push origin ${branch} --follow-tags`);
if (await isCurrentBranchTracked()) {
await execute(`git pull --rebase`);
await execute(`git push origin ${branch} --follow-tags`);
} else {
await execute(`git push --set-upstream origin ${branch}`);
// HACK: This way GitHub actions for `pushes` on new branches are picked up
await execute(`git push --force`);
}
};
/**
* Force pushes the changes to the documentation update branch
* and creates a new PR if there is none available.
* and creates a new PR if there is none available with review
* request for `REVIEWERS`.
* @param {string} branch
* @param {string} base
* @param {string} email
@ -50,7 +60,11 @@ const pushChanges = async (branch, email, name, message) => {
*/
const createPR = async (branch, base, email, name, message) => {
await createCommit(email, name, message);
await execute(`git checkout -b ${branch}`);
if (getCurrentBranchName() !== branch) {
await execute(`git checkout -b ${branch}`);
}
await execute(`git push --force --set-upstream origin ${branch}`);
console.log(`Changes pushed to ${branch}`);
@ -81,11 +95,86 @@ const createPR = async (branch, base, email, name, message) => {
});
console.log(`PR created (#${result.data.id})`);
await octokit.pulls.requestReviewers({
owner: context.repo.owner,
repo: context.repo.repo,
pull_number: result.data.id,
reviewers: REVIEWERS,
});
}
};
/**
* Returns an array with the remote branches.
* @returns {Promise<string[]>}
*/
const getRemoteBranches = async () => {
const { stdout } = await execute(`git branch -r`);
/**
* The output of the command above is similar to
*
* ```
* origin/HEAD -> origin/main
* origin/v15-x-y
* origin/v14-x-y
* origin/feat/i18n
* ```
*
* We do not need `HEAD` so we filter it out
*/
const branches = stdout
.split('\n')
.map((line) => line.trim())
.filter((line) => !line.includes('->'));
return branches;
};
/**
* Determines if the active branch is current tracked in the
* remote by calling `git branch -vv` and parsing the output.
*
* The output has the following form:
*
* ```plain
* bots 3527526ae110 fix: docs automerge
* * versioned-docs 0fe736ed2529 [origin/versioned-docs] feat: versioned docs
* ```
*
* In the case above the branch `bots` is not tracked (no
* `[origin/]` information) and is not the active branch (no `*`).
*
* @returns {Promise<boolean>}
*/
const isCurrentBranchTracked = async () => {
const { stdout } = await execute(`git branch -vv`);
const lines = stdout.trim().split('\n');
const current = lines.filter((line) => line.trim().startsWith('*'));
if (!current) {
throw new Error(`Couldn't determine current branch`);
}
return current.includes(`[origin/`);
};
/**
* Returns the name of the current branch
* @returns {Promise<string>}
*/
const getCurrentBranchName = async () => {
const { stdout } = await execute(`git branch --show-current`);
return stdout;
};
module.exports = {
createPR,
getChanges,
getCurrentBranchName,
getRemoteBranches,
isCurrentBranchTracked,
pushChanges,
};

7
versions-info.json Normal file
Просмотреть файл

@ -0,0 +1,7 @@
[
{
"label": "Current",
"href": "/docs/latest",
"target": "_self"
}
]