Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add TypeScript script that replaces website-sync #858

Open
wants to merge 3 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -2,3 +2,4 @@
.idea
/docs/variables.mk.local
/node_modules/
/publish-technical-documentation/node_modules/
30 changes: 30 additions & 0 deletions publish-technical-documentation/action.mts
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import core from "@actions/core";
import github from "@actions/github";
import { Octokit } from "octokit";

import { publish } from "./lib/publish.mjs";

const sourceDirectory = core.getInput("source-directory");
const websiteDirectory = core.getInput("website-directory");

const token = core.getInput("token");
const octokit = new Octokit({ auth: token });

export async function main() {
publish(
octokit,
{
name: github.context.repo.repo,
branch: github.context.ref,
repositoryPath: ".",
subdirectoryPath: sourceDirectory,
},
{
repositoryPath: "website",
subdirectoryPath: websiteDirectory,
}
).catch((error) => {
console.error(error);
core.setFailed(error.message);
});
}
32 changes: 16 additions & 16 deletions publish-technical-documentation/action.yml
Original file line number Diff line number Diff line change
Expand Up @@ -10,10 +10,10 @@ description: |
id-token: write
```
inputs:
source_directory:
source-directory:
default: docs/sources
description: Path to source directory, relative to the project root, to sync documentation from.
website_directory:
website-directory:
description: Website directory to sync the documentation to.
required: true
runs:
Expand All @@ -22,7 +22,7 @@ runs:
- name: Build website
shell: bash
run: |
docker run -v "${PWD}/${{ inputs.source_directory }}:/hugo/${{ inputs.website_directory }}" --rm grafana/docs-base:latest /bin/bash -c 'make hugo'
docker run -v "${PWD}/${{ inputs.source-directory }}:/hugo/${{ inputs.website-directory }}" --rm grafana/docs-base:latest /bin/bash -c 'make hugo'

- id: get-secrets
uses: grafana/shared-workflows/actions/get-vault-secrets@main
Expand All @@ -35,23 +35,23 @@ runs:
id: app-token
with:
app-id: ${{ env.PUBLISH_TECHNICAL_DOCUMENTATION_APP_ID }}
owner: grafana
private-key: ${{ env.PUBLISH_TECHNICAL_DOCUMENTATION_PRIVATE_KEY }}

- name: Checkout sync action
uses: actions/checkout@v4
- uses: actions/checkout@v4
with:
path: .github/actions/website-sync
repository: grafana/website-sync
path: website
repository: grafana/website
sparse-checkout: |
content/docs
token: ${{ steps.app-token.outputs.token }}

- name: Sync to the website repository
uses: ./.github/actions/website-sync
id: publish
uses: actions/github-script@v7
with:
repository: grafana/website
branch: master
host: github.com
github_pat: grafanabot:${{ steps.app-token.outputs.token }}
source_folder: ${{ inputs.source_directory }}
target_folder: ${{ inputs.website_directory }}
script: |
const { main } = await import('${{ github.action_path }}/dist/action.mjs');

await main();
source-directory: ${{ inputs.source-directory }}
token: ${{ steps.app-token.outputs.token }}
website-directory: ${{ inputs.website-directory }}
22 changes: 22 additions & 0 deletions publish-technical-documentation/dist/action.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import core from "@actions/core";
import github from "@actions/github";
import { Octokit } from "octokit";
import { publish } from "./lib/publish.mjs";
const sourceDirectory = core.getInput("source-directory");
const websiteDirectory = core.getInput("website-directory");
const token = core.getInput("token");
const octokit = new Octokit({ auth: token });
export async function main() {
publish(octokit, {
name: github.context.repo.repo,
branch: github.context.ref,
repositoryPath: ".",
subdirectoryPath: sourceDirectory,
}, {
repositoryPath: "website",
subdirectoryPath: websiteDirectory,
}).catch((error) => {
console.error(error);
core.setFailed(error.message);
});
}
176 changes: 176 additions & 0 deletions publish-technical-documentation/dist/lib/publish.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,176 @@
// Copyright 2024 Grafana Labs
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
import { spawn } from "node:child_process";
import * as readline from "node:readline/promises";
import * as fs from "node:fs";
import path from "node:path";
var Status;
(function (Status) {
Status["UNMODIFIED"] = " ";
Status["MODIFIED"] = "M";
Status["ADDED"] = "A";
Status["DELETED"] = "D";
Status["RENAMED"] = "R";
Status["COPIED"] = "C";
Status["UNMERGED"] = "U";
Status["UNTRACKED"] = "?";
Status["IGNORED"] = "!";
})(Status || (Status = {}));
// Git status porcelain v1 format:
// https://git-scm.com/docs/git-status#_short_format
// XY PATH
// XY ORIG_PATH -> PATH
function parsePorcelainV1(line) {
if (line.length < 4) {
throw new Error(`Invalid porcelain v1 line: ${line}`);
}
const indexStatus = line[0];
const workingTreeStatus = line[1];
const rest = line.slice(3);
const [originalPath, path] = rest.split(" -> ");
if (!path) {
return {
indexStatus,
workingTreeStatus,
path: rest,
};
}
return {
indexStatus,
workingTreeStatus,
path,
originalPath,
};
}
async function rsync(source, destination) {
return new Promise((resolve, reject) => {
const rsync = spawn("rsync", [
"-a",
"--quiet",
"--delete",
source,
destination,
]);
let stderrOutput = "";
const stderr = readline.createInterface({ input: rsync.stderr });
stderr.on("line", (line) => {
stderrOutput += line;
});
rsync.on("close", (code) => {
if (code !== 0) {
reject(`${stderrOutput}\nrsync process exited with code ${code}`);
}
resolve();
});
});
}
// Return a list of all unstaged Git files in the specified subdirectory.
async function getUnstagedFiles(repository, subdirectory) {
return new Promise((resolve, reject) => {
const git = spawn("git", ["status", "--porcelain=v1", "--", subdirectory], {
cwd: repository,
});
const files = [];
const stdout = readline.createInterface({ input: git.stdout });
stdout.on("line", (line) => {
files.push(parsePorcelainV1(line));
});
let stderrOutput = "";
const stderr = readline.createInterface({ input: git.stderr });
stderr.on("line", (line) => {
stderrOutput += line;
});
git.on("close", (code) => {
if (code !== 0) {
reject(`${stderrOutput}\ngit process exited with code ${code}`);
}
resolve(files);
});
});
}
async function baseTreeSha(octokit, repo, branch) {
return (await octokit.rest.git.getTree({
owner: "grafana",
repo,
tree_sha: `heads/${branch}`,
})).data.sha;
}
export async function publish(octokit, source, website) {
rsync(path.join(source.repositoryPath, source.subdirectoryPath) + "/", path.join(website.repositoryPath, website.subdirectoryPath));
const files = await getUnstagedFiles(website.repositoryPath, website.subdirectoryPath);
const baseTree = await baseTreeSha(octokit, "website", "master");
const newTree = (await octokit.rest.git.createTree({
owner: "grafana",
repo: "website",
base_tree: baseTree,
tree: files.map((file) => {
if (file.workingTreeStatus === Status.DELETED) {
return {
mode: "100644",
path: file.path,
sha: null,
type: "blob",
};
}
else {
const fileInfo = fs.statSync(path.join(website.repositoryPath, file.path));
if (fileInfo.isDirectory()) {
return {
mode: "040000",
path: file.path.replace(/\/$/, ""),
type: "tree",
sha: null,
};
}
return {
content: fs
.readFileSync(path.join(website.repositoryPath, file.path))
.toString(),
mode: "100644",
path: file.path,
type: "blob",
};
}
}),
})).data.sha;
const commit = (await octokit.rest.git.createCommit({
owner: "grafana",
repo: "website",
message: `Publish from grafana/${source.name}:${source.branch}/${source.subdirectoryPath}\n` +
"\n" +
"Co-authored-by: Jack Baldry <[email protected]>",
tree: newTree,
parents: [baseTree],
})).data.sha;
octokit.rest.git.deleteRef({
owner: "grafana",
repo: "website",
ref: "heads/jdb/2024-09-test-publish-technical-documentation",
});
const websiteCommit = octokit.rest.git.createRef({
owner: "grafana",
repo: "website",
ref: "refs/heads/jdb/2024-09-test-publish-technical-documentation",
sha: commit,
});
// const websiteCommit = (
// await octokit.rest.git.updateRef({
// owner: "grafana",
// repo: "website",
// ref: "heads/master",
// sha: commit,
// })
// ).data;
return (await websiteCommit).data.object.sha;
}
42 changes: 42 additions & 0 deletions publish-technical-documentation/dist/local.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
import { env } from "node:process";
import { Octokit } from "octokit";
import { publish } from "./lib/publish.mjs";
try {
const token = env.GITHUB_TOKEN;
if (!token) {
throw new Error("Environment variable GITHUB_TOKEN is required");
}
const octokit = new Octokit({ auth: token });
const sourceName = env.PUBLISH_TECHNICAL_DOCUMENTATION_SOURCE_NAME;
if (!sourceName) {
throw new Error("Environment variable PUBLISH_TECHNICAL_DOCUMENTATION_SOURCE_NAME is required");
}
const sourceBranch = env.PUBLISH_TECHNICAL_DOCUMENTATION_SOURCE_BRANCH || "main";
const sourceRepositoryPath = env.PUBLISH_TECHNICAL_DOCUMENTATION_SOURCE_REPOSITORY_PATH;
if (!sourceRepositoryPath) {
throw new Error("Environment variable PUBLISH_TECHNICAL_DOCUMENTATION_SOURCE_REPOSITORY_PATH is required");
}
const sourceSubdirectoryPath = env.PUBLISH_TECHNICAL_DOCUMENTATION_SOURCE_SUBDIRECTORY_PATH ||
"docs/sources";
const websiteRepositoryPath = env.PUBLISH_TECHNICAL_DOCUMENTATION_WEBSITE_REPOSITORY;
if (!websiteRepositoryPath) {
throw new Error("Environment variable PUBLISH_TECHNICAL_DOCUMENTATION_WEBSITE_REPOSITORY_PATH is required");
}
const websiteSubdirectoryPath = env.PUBLISH_TECHNICAL_DOCUMENTATION_WEBSITE_SUBDIRECTORY_PATH;
if (!websiteSubdirectoryPath) {
throw new Error("Environment variable PUBLISH_TECHNICAL_DOCUMENTATION_WEBSITE_SUBDIRECTORY_PATH is required");
}
const sha = await publish(octokit, {
name: sourceName,
branch: sourceBranch,
repositoryPath: sourceRepositoryPath,
subdirectoryPath: sourceSubdirectoryPath,
}, {
repositoryPath: websiteRepositoryPath,
subdirectoryPath: websiteSubdirectoryPath,
});
console.log(sha);
}
catch (error) {
console.error(error);
}
Loading