feat: show call participants in room list (Discord-style)
Docker / Docker Buildx (push) Has been cancelled
Build Debian package / Build package (release) Has been cancelled
Build and Deploy / prepare (release) Has been cancelled
Deploy release / Deploy to Cloudflare Pages (release) Has been cancelled
Build and Deploy / Trigger Pro pipeline (release) Has been cancelled
Build and Deploy / Windows arm64 (release) Has been cancelled
Build and Deploy / Windows x64 (release) Has been cancelled
Build and Deploy / macOS (release) Has been cancelled
Build and Deploy / Linux amd64 (sqlcipher static) (release) Has been cancelled
Build and Deploy / Linux arm64 (sqlcipher static) (release) Has been cancelled
Build and Deploy / ${{ needs.prepare.outputs.deploy == 'true' && 'Deploy' || 'Deploy (dry-run)' }} (release) Has been cancelled
Build and Deploy / Deploy builds to ESS (release) Has been cancelled
Docker / Docker Buildx (push) Has been cancelled
Build Debian package / Build package (release) Has been cancelled
Build and Deploy / prepare (release) Has been cancelled
Deploy release / Deploy to Cloudflare Pages (release) Has been cancelled
Build and Deploy / Trigger Pro pipeline (release) Has been cancelled
Build and Deploy / Windows arm64 (release) Has been cancelled
Build and Deploy / Windows x64 (release) Has been cancelled
Build and Deploy / macOS (release) Has been cancelled
Build and Deploy / Linux amd64 (sqlcipher static) (release) Has been cancelled
Build and Deploy / Linux arm64 (sqlcipher static) (release) Has been cancelled
Build and Deploy / ${{ needs.prepare.outputs.deploy == 'true' && 'Deploy' || 'Deploy (dry-run)' }} (release) Has been cancelled
Build and Deploy / Deploy builds to ESS (release) Has been cancelled
This commit is contained in:
@@ -0,0 +1,64 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
import * as fs from "node:fs";
|
||||
import { exec } from "node:child_process";
|
||||
|
||||
const includeJSSDK = process.argv.includes("--include-js-sdk");
|
||||
const ignore: string[] = [];
|
||||
|
||||
ignore.push(...Object.values<string>(JSON.parse(fs.readFileSync(`${__dirname}/../components.json`, "utf-8"))));
|
||||
ignore.push("/index.ts");
|
||||
ignore.push("/jest-matrix-react.tsx");
|
||||
ignore.push("/customisations/");
|
||||
ignore.push("/test-utils/");
|
||||
|
||||
// The following ignores are temporary and false-positives which need to be fixed
|
||||
ignore.push("/useLocalStorageState.ts");
|
||||
ignore.push("/blurhash.worker.ts");
|
||||
ignore.push("/OpenSpotlightPayload.ts");
|
||||
ignore.push("/PinnedMessageBadge.tsx");
|
||||
ignore.push("/editor/mock.ts");
|
||||
ignore.push("DeviceIsolationModeController.ts");
|
||||
ignore.push("urls.ts");
|
||||
ignore.push("/json.ts");
|
||||
ignore.push("/ReleaseAnnouncementStore.ts");
|
||||
ignore.push("/WidgetLayoutStore.ts");
|
||||
ignore.push("/common.ts");
|
||||
|
||||
// We ignore js-sdk by default as it may export for other non element-web projects
|
||||
if (!includeJSSDK) ignore.push("matrix-js-sdk");
|
||||
|
||||
const command = `pnpm ts-prune --ignore "${ignore.join("|")}" | grep -v "(used in module)"`;
|
||||
|
||||
exec(command, (error, stdout, stderr) => {
|
||||
if (error) throw error;
|
||||
// We have to do this as piping the output of ts-prune causes the return
|
||||
// code to be 0
|
||||
if (stderr) throw Error(stderr);
|
||||
|
||||
let lines = stdout.split("\n");
|
||||
// Remove the first line as that is the command that was being run and we
|
||||
// log that only in case of an error
|
||||
lines.splice(0, 1);
|
||||
// Remove the last line as it is empty
|
||||
lines.pop();
|
||||
|
||||
// ts-prune has bug where if the unused export is in a dependency, the path
|
||||
// won't have an "/" character at the start, so we try to fix that for
|
||||
// better UX
|
||||
// TODO: This might break on Windows
|
||||
lines = lines.reduce<string[]>((newLines, line) => {
|
||||
if (!line.startsWith("/")) newLines.push("/" + line);
|
||||
else newLines.push(line);
|
||||
return newLines;
|
||||
}, []);
|
||||
|
||||
// If an unused export has been found, we error
|
||||
if (lines.length > 0) {
|
||||
console.log(`Command that was run: ${command}`);
|
||||
console.log(lines.join("\n"));
|
||||
throw Error("Unused exports found!");
|
||||
}
|
||||
|
||||
console.log("Success - no unused exports found!");
|
||||
});
|
||||
@@ -0,0 +1,28 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
set -ex
|
||||
|
||||
# Automatically link to develop if we're building develop, but only if the caller
|
||||
# hasn't asked us to build something else
|
||||
BRANCH=$(git rev-parse --abbrev-ref HEAD)
|
||||
if [[ $USE_CUSTOM_SDKS == false ]] && [[ $BRANCH == 'develop' ]]
|
||||
then
|
||||
echo "using develop dependencies for react-sdk and js-sdk"
|
||||
USE_CUSTOM_SDKS=true
|
||||
JS_SDK_BRANCH='develop'
|
||||
fi
|
||||
|
||||
if [[ $USE_CUSTOM_SDKS == false ]]
|
||||
then
|
||||
echo "skipping js-sdk install: USE_CUSTOM_SDKS is false"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
echo "Linking js-sdk"
|
||||
git clone --depth 1 --branch $JS_SDK_BRANCH "$JS_SDK_REPO" js-sdk
|
||||
cd js-sdk
|
||||
pnpm install
|
||||
cd ../
|
||||
|
||||
echo "Setting up element-web with js-sdk package"
|
||||
pnpm link ./js-sdk
|
||||
@@ -0,0 +1,21 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
set -ex
|
||||
|
||||
BRANCH=$(git rev-parse --abbrev-ref HEAD)
|
||||
|
||||
DIR=$(dirname "$0")
|
||||
|
||||
# If the branch comes out as HEAD then we're probably checked out to a tag, so if the thing is *not*
|
||||
# coming out as HEAD then we're on a branch. When we're on a branch, we want to resolve ourselves to
|
||||
# a few SHAs rather than a version.
|
||||
if [[ $BRANCH != HEAD && ! $BRANCH =~ heads/v.+ ]]
|
||||
then
|
||||
DIST_VERSION=$("$DIR"/get-version-from-git.sh)
|
||||
else
|
||||
DIST_VERSION=$(git describe --abbrev=0 --tags)
|
||||
fi
|
||||
|
||||
DIST_VERSION=$("$DIR"/normalize-version.sh "$DIST_VERSION")
|
||||
|
||||
VERSION=$DIST_VERSION pnpm --dir apps/web build
|
||||
@@ -0,0 +1,96 @@
|
||||
#!/bin/bash
|
||||
|
||||
set -x
|
||||
|
||||
deforg="$1"
|
||||
defrepo="$2"
|
||||
defbranch="$3"
|
||||
|
||||
rm -r "$defrepo" || true
|
||||
|
||||
# figure out where to look for pull requests:
|
||||
# - We may have been told an explicit repo via the PR_ORG/PR_REPO/PR_NUMBER env vars
|
||||
# - otherwise, check the $GITHUB_ env vars which are set by Github Actions
|
||||
# - failing that, fall back to the element-hq/element-web repo.
|
||||
#
|
||||
# in ether case, the PR_NUMBER variable must be set explicitly.
|
||||
default_org_repo=${GITHUB_REPOSITORY:-"element-hq/element-web"}
|
||||
PR_ORG=${PR_ORG:-${default_org_repo%%/*}}
|
||||
PR_REPO=${PR_REPO:-${default_org_repo##*/}}
|
||||
|
||||
# A function that clones a branch of a repo based on the org, repo and branch
|
||||
clone() {
|
||||
org=$1
|
||||
repo=$2
|
||||
branch=$3
|
||||
if [ -n "$branch" ]
|
||||
then
|
||||
echo "Trying to use $org/$repo#$branch"
|
||||
# Disable auth prompts: https://serverfault.com/a/665959
|
||||
if GIT_TERMINAL_PROMPT=0 git clone "https://github.com/$org/$repo.git" "$repo" --branch "$branch" --depth 1; then
|
||||
git -C "$repo" log -1
|
||||
exit 0
|
||||
fi
|
||||
fi
|
||||
}
|
||||
|
||||
# A function that gets info about a PR from the GitHub API based on its number
|
||||
getPRInfo() {
|
||||
number=$1
|
||||
if [ -n "$number" ]; then
|
||||
echo "Getting info about a PR with number $number"
|
||||
|
||||
apiEndpoint="https://api.github.com/repos/$PR_ORG/$PR_REPO/pulls/$number"
|
||||
|
||||
head=$(curl "$apiEndpoint" | jq -r '.head.label')
|
||||
fi
|
||||
}
|
||||
|
||||
# Some CIs don't give us enough info, so we just get the PR number and ask the
|
||||
# GH API for more info - "fork:branch". Some give us this directly.
|
||||
if [ -n "$PR_NUMBER" ]; then
|
||||
# GitHub
|
||||
getPRInfo "$PR_NUMBER"
|
||||
elif [ -n "$REVIEW_ID" ]; then
|
||||
# Netlify
|
||||
getPRInfo "$REVIEW_ID"
|
||||
fi
|
||||
|
||||
# for forks, $head will be in the format "fork:branch", so we split it by ":"
|
||||
# into an array. On non-forks, this has the effect of splitting into a single
|
||||
# element array given ":" shouldn't appear in the head - it'll just be the
|
||||
# branch name. Based on the results, we clone.
|
||||
# shellcheck disable=SC2206
|
||||
BRANCH_ARRAY=(${head//:/ })
|
||||
TRY_ORG=$deforg
|
||||
TRY_BRANCH=${BRANCH_ARRAY[0]}
|
||||
if [[ "$head" == *":"* ]]; then
|
||||
# ... but only match that fork if it's a real fork
|
||||
if [ "${BRANCH_ARRAY[0]}" != "$PR_ORG" ]; then
|
||||
TRY_ORG=${BRANCH_ARRAY[0]}
|
||||
fi
|
||||
TRY_BRANCH=${BRANCH_ARRAY[1]}
|
||||
fi
|
||||
clone "$TRY_ORG" "$defrepo" "$TRY_BRANCH"
|
||||
|
||||
# For merge queue runs we need to extract the temporary branch name
|
||||
# the ref_name will look like `gh-readonly-queue/<branch>/pr-<number>-<sha>`
|
||||
if [[ "$GITHUB_EVENT_NAME" == "merge_group" ]]; then
|
||||
withoutPrefix=${GITHUB_REF_NAME#gh-readonly-queue/}
|
||||
clone "$deforg" "$defrepo" "${withoutPrefix%%/pr-*}"
|
||||
fi
|
||||
|
||||
# Try the target branch of the push or PR, or the branch that was pushed to
|
||||
# (ie. the 'master' branch should use matching 'master' dependencies)
|
||||
base_or_branch=$GITHUB_BASE_REF
|
||||
if [[ "$GITHUB_EVENT_NAME" == "push" ]]; then
|
||||
base_or_branch=${GITHUB_REF}
|
||||
fi
|
||||
if [ -n "$base_or_branch" ]; then
|
||||
clone "$deforg" "$defrepo" "$base_or_branch"
|
||||
fi
|
||||
|
||||
# Try HEAD which is the branch name in Netlify (not BRANCH which is pull/xxxx/head for PR builds)
|
||||
clone "$deforg" "$defrepo" "$HEAD"
|
||||
# Use the default branch as the last resort.
|
||||
clone "$deforg" "$defrepo" "$defbranch"
|
||||
@@ -0,0 +1,667 @@
|
||||
import fs from "node:fs/promises";
|
||||
import path from "node:path";
|
||||
import YAML from "yaml";
|
||||
import parseArgs from "minimist";
|
||||
import cronstrue from "cronstrue";
|
||||
import _ from "lodash";
|
||||
import url from "node:url";
|
||||
|
||||
/**
|
||||
* Generates unique ID strings (incremental base36) representing the given inputs.
|
||||
*/
|
||||
class IdGenerator<T> {
|
||||
private id = 0;
|
||||
private map = new Map<T, string>();
|
||||
|
||||
public get(s: T): string {
|
||||
if (this.map.has(s)) return this.map.get(s)!;
|
||||
const id = "ID" + this.id.toString(36).toLowerCase();
|
||||
this.map.set(s, id);
|
||||
this.id++;
|
||||
return id;
|
||||
}
|
||||
|
||||
public debug(): void {
|
||||
console.log("```");
|
||||
console.log(this.map);
|
||||
console.log("```");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Type representing a node on a graph with additional metadata
|
||||
*/
|
||||
interface Node {
|
||||
// Workflows are keyed by project/name??id
|
||||
// Jobs are keyed by id
|
||||
// Triggers are keyed by id
|
||||
id: string;
|
||||
name: string;
|
||||
shape:
|
||||
| "round edges"
|
||||
| "stadium"
|
||||
| "subroutine"
|
||||
| "cylinder"
|
||||
| "circle"
|
||||
| "flag"
|
||||
| "rhombus"
|
||||
| "hexagon"
|
||||
| "parallelogram"
|
||||
| "parallelogram_alt"
|
||||
| "trapezoid"
|
||||
| "trapezoid_alt"
|
||||
| "double_circle";
|
||||
link?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Type representing a directed edge on a graph with an optional label
|
||||
*/
|
||||
type Edge<T> = [source: T, destination: T, label?: string];
|
||||
|
||||
class Graph<T extends Node> {
|
||||
public nodes = new Map<string, T>();
|
||||
public edges: Edge<T>[] = [];
|
||||
|
||||
public addNode(node: T): void {
|
||||
if (!this.nodes.has(node.id)) {
|
||||
this.nodes.set(node.id, node);
|
||||
}
|
||||
}
|
||||
|
||||
public removeNode(node: T): Edge<T>[] {
|
||||
if (!this.nodes.has(node.id)) return [];
|
||||
this.nodes.delete(node.id);
|
||||
const [removedEdges, keptEdges] = _.partition(
|
||||
this.edges,
|
||||
([source, destination]) => source === node || destination === node,
|
||||
);
|
||||
this.edges = keptEdges;
|
||||
return removedEdges;
|
||||
}
|
||||
|
||||
public addEdge(source: T, destination: T, label?: string): void {
|
||||
if (this.edges.some(([_source, _destination]) => _source === source && _destination === destination)) return;
|
||||
this.edges.push([source, destination, label]);
|
||||
}
|
||||
|
||||
// Removes nodes without any edges
|
||||
public cull(): void {
|
||||
const seenNodes = new Set<Node>();
|
||||
this.edges.forEach(([source, destination]) => {
|
||||
seenNodes.add(source);
|
||||
seenNodes.add(destination);
|
||||
});
|
||||
this.nodes.forEach((node) => {
|
||||
if (!seenNodes.has(node)) {
|
||||
this.nodes.delete(node.id);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public get roots(): Set<T> {
|
||||
const roots = new Set(this.nodes.values());
|
||||
this.edges.forEach(([source, destination]) => {
|
||||
roots.delete(destination);
|
||||
});
|
||||
return roots;
|
||||
}
|
||||
|
||||
private componentsRecurse(root: T, visited: Set<T>): T[] {
|
||||
if (visited.has(root)) return [root];
|
||||
visited.add(root);
|
||||
|
||||
const neighbours = [root];
|
||||
this.edges.forEach(([source, destination]) => {
|
||||
if (source === root) {
|
||||
neighbours.push(...this.componentsRecurse(destination, visited));
|
||||
} else if (destination === root) {
|
||||
neighbours.push(...this.componentsRecurse(source, visited));
|
||||
}
|
||||
});
|
||||
|
||||
return neighbours;
|
||||
}
|
||||
|
||||
public get components(): Graph<T>[] {
|
||||
const graphs: Graph<T>[] = [];
|
||||
const visited = new Set<T>();
|
||||
this.nodes.forEach((node) => {
|
||||
if (visited.has(node)) return;
|
||||
|
||||
const graph = new Graph<T>();
|
||||
graphs.push(graph);
|
||||
|
||||
const nodes = this.componentsRecurse(node, visited);
|
||||
nodes.forEach((node) => {
|
||||
graph.addNode(node);
|
||||
this.edges.forEach((edge) => {
|
||||
if (edge[0] === node || edge[1] === node) {
|
||||
graph.addEdge(...edge);
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
return graphs;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Type representing a GitHub project
|
||||
*/
|
||||
interface Project {
|
||||
url: string;
|
||||
name: string;
|
||||
path: string;
|
||||
workflows: Map<string, Workflow>;
|
||||
}
|
||||
|
||||
/**
|
||||
* Type representing a GitHub Actions Workflow
|
||||
*/
|
||||
interface Workflow extends Node {
|
||||
path: string;
|
||||
project: Project;
|
||||
jobs: Job[];
|
||||
on: WorkflowYaml["on"];
|
||||
}
|
||||
|
||||
/**
|
||||
* Type representing a job within a GitHub Actions Workflow
|
||||
*/
|
||||
interface Job extends Node {
|
||||
jobId: string; // id relative to workflow
|
||||
needs?: string[];
|
||||
strategy?: {
|
||||
matrix: {
|
||||
[key: string]: string[];
|
||||
} & {
|
||||
include?: Record<string, string>[];
|
||||
exclude?: Record<string, string>[];
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Type representing the YAML structure of a GitHub Actions Workflow file
|
||||
*/
|
||||
interface WorkflowYaml {
|
||||
name: string;
|
||||
on: {
|
||||
workflow_run?: {
|
||||
workflows: string[];
|
||||
}; // Magic
|
||||
workflow_call?: {}; // Reusable
|
||||
workflow_dispatch?: {}; // Manual
|
||||
pull_request?: {};
|
||||
merge_group?: {};
|
||||
push?: {
|
||||
tags?: string[];
|
||||
branches?: string[];
|
||||
};
|
||||
schedule?: { cron: string }[];
|
||||
release?: {};
|
||||
//
|
||||
label?: {};
|
||||
issues?: {};
|
||||
};
|
||||
jobs: {
|
||||
[job: string]: {
|
||||
name?: string;
|
||||
needs?: string | string[];
|
||||
strategy?: Job["strategy"];
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Type representing a trigger of a GitHub Actions Workflow
|
||||
*/
|
||||
type Trigger = Node;
|
||||
|
||||
// TODO workflow_call reusables
|
||||
/* eslint-disable @typescript-eslint/naming-convention */
|
||||
const TRIGGERS: {
|
||||
[key in keyof WorkflowYaml["on"]]: (
|
||||
data: NonNullable<WorkflowYaml["on"][key]>,
|
||||
workflow: Workflow,
|
||||
) => Trigger | Trigger[];
|
||||
} = {
|
||||
workflow_dispatch: () => ({
|
||||
id: "on:workflow_dispatch",
|
||||
name: "Manual",
|
||||
shape: "circle",
|
||||
}),
|
||||
issues: (_, { project }) => ({ id: `on:issues/${project.name}`, name: `${project.name} Issues`, shape: "circle" }),
|
||||
label: (_, { project }) => ({ id: "on:label", name: "on: Label", shape: "circle" }),
|
||||
release: (_, { project }) => ({
|
||||
id: `on:release/${project.name}`,
|
||||
name: `${project.name} Release`,
|
||||
shape: "circle",
|
||||
}),
|
||||
push: (data, { project }) => {
|
||||
const nodes: Trigger[] = [];
|
||||
data.tags?.forEach((tag) => {
|
||||
const name = `Push ${project.name}<br>tag ${tag}`;
|
||||
nodes.push({ id: `on:push/${project.name}/tag/${tag}`, name, shape: "circle" });
|
||||
});
|
||||
data.branches?.forEach((branch) => {
|
||||
const name = `Push ${project.name}<br>${branch}`;
|
||||
nodes.push({ id: `on:push/${project.name}/branch/${branch}`, name, shape: "circle" });
|
||||
});
|
||||
return nodes;
|
||||
},
|
||||
schedule: (data) =>
|
||||
data.map(({ cron }) => ({
|
||||
id: `on:schedule/${cron}`,
|
||||
name: cronstrue.toString(cron).replaceAll(", ", "<br>"),
|
||||
shape: "circle",
|
||||
})),
|
||||
pull_request: (_, { project }) => ({
|
||||
id: `on:pull_request/${project.name}`,
|
||||
name: `Pull Request<br>${project.name}`,
|
||||
shape: "circle",
|
||||
}),
|
||||
// TODO should we be just dropping these?
|
||||
workflow_run: (data) => data.workflows.map((parent) => workflows.get(parent)).filter(Boolean) as Workflow[],
|
||||
};
|
||||
/* eslint-enable @typescript-eslint/naming-convention */
|
||||
|
||||
const triggers = new Map<string, Trigger>(); // keyed by trigger id
|
||||
const projects = new Map<string, Project>(); // keyed by project name
|
||||
const workflows = new Map<string, Workflow>(); // keyed by workflow name
|
||||
|
||||
function getTriggerNodes<K extends keyof WorkflowYaml["on"]>(key: K, workflow: Workflow, on?: string[]): Trigger[] {
|
||||
if (!TRIGGERS[key]) return [];
|
||||
|
||||
if (on && !on.includes(key)) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const data = workflow.on[key]!;
|
||||
const nodes = toArray(TRIGGERS[key]!(data, workflow));
|
||||
return nodes.map((node) => {
|
||||
if (triggers.has(node.id)) return triggers.get(node.id)!;
|
||||
triggers.set(node.id, node);
|
||||
return node;
|
||||
});
|
||||
}
|
||||
|
||||
function readFile(...pathSegments: string[]): Promise<string> {
|
||||
return fs.readFile(path.join(...pathSegments), { encoding: "utf-8" });
|
||||
}
|
||||
|
||||
async function readJson<T extends object>(...pathSegments: string[]): Promise<T> {
|
||||
return JSON.parse(await readFile(...pathSegments));
|
||||
}
|
||||
|
||||
async function readYaml<T extends object>(...pathSegments: string[]): Promise<T> {
|
||||
return YAML.parse(await readFile(...pathSegments));
|
||||
}
|
||||
|
||||
function toArray<T>(v: T | T[]): T[] {
|
||||
return Array.isArray(v) ? v : [v];
|
||||
}
|
||||
|
||||
function cartesianProduct<T>(sets: T[][]): T[][] {
|
||||
return sets.reduce<T[][]>(
|
||||
(results, ids) =>
|
||||
results
|
||||
.map((result) => ids.map((id) => [...result, id]))
|
||||
.reduce((nested, result) => [...nested, ...result]),
|
||||
[[]],
|
||||
);
|
||||
}
|
||||
|
||||
function shallowCompare(obj1: Record<string, any>, obj2: Record<string, any>): boolean {
|
||||
return (
|
||||
Object.keys(obj1).length === Object.keys(obj2).length &&
|
||||
Object.keys(obj1).every((key) => obj1[key] === obj2[key])
|
||||
);
|
||||
}
|
||||
|
||||
class MermaidFlowchartPrinter {
|
||||
private static INDENT = 4;
|
||||
private currentIndent = 0;
|
||||
private text = "";
|
||||
private readonly markdown: boolean;
|
||||
public readonly idGenerator = new IdGenerator();
|
||||
|
||||
private print(text: string): void {
|
||||
this.text += " ".repeat(this.currentIndent) + text + "\n";
|
||||
}
|
||||
|
||||
public finish(print: boolean): string {
|
||||
this.indent(-1);
|
||||
if (this.markdown) this.print("```\n");
|
||||
if (print) console.log(this.text);
|
||||
return this.text;
|
||||
}
|
||||
|
||||
private indent(delta = 1): void {
|
||||
this.currentIndent += delta * MermaidFlowchartPrinter.INDENT;
|
||||
}
|
||||
|
||||
public constructor(direction: "TD" | "TB" | "BT" | "RL" | "LR", title?: string, markdown = false) {
|
||||
this.markdown = markdown;
|
||||
if (this.markdown) {
|
||||
this.print("```mermaid");
|
||||
}
|
||||
// Print heading
|
||||
if (title) {
|
||||
this.print("---");
|
||||
this.print(`title: ${title}`);
|
||||
this.print("---");
|
||||
}
|
||||
this.print(`flowchart ${direction}`);
|
||||
this.indent();
|
||||
}
|
||||
|
||||
public subgraph(id: string, name: string, fn: () => void): void {
|
||||
this.print(`subgraph ${this.idGenerator.get(id)}["${name}"]`);
|
||||
this.indent();
|
||||
fn();
|
||||
this.indent(-1);
|
||||
this.print("end");
|
||||
}
|
||||
|
||||
public node(node: Node): void {
|
||||
const id = this.idGenerator.get(node.id);
|
||||
const name = node.name.replaceAll('"', "'");
|
||||
switch (node.shape) {
|
||||
case "round edges":
|
||||
this.print(`${id}("${name}")`);
|
||||
break;
|
||||
case "stadium":
|
||||
this.print(`${id}(["${name}"])`);
|
||||
break;
|
||||
case "subroutine":
|
||||
this.print(`${id}[["${name}"]]`);
|
||||
break;
|
||||
case "cylinder":
|
||||
this.print(`${id}[("${name}")]`);
|
||||
break;
|
||||
case "circle":
|
||||
this.print(`${id}(("${name}"))`);
|
||||
break;
|
||||
case "flag":
|
||||
this.print(`${id}>"${name}"]`);
|
||||
break;
|
||||
case "rhombus":
|
||||
this.print(`${id}{"${name}"}`);
|
||||
break;
|
||||
case "hexagon":
|
||||
this.print(`${id}{{"${name}"}}`);
|
||||
break;
|
||||
case "parallelogram":
|
||||
this.print(`${id}[/"${name}"/]`);
|
||||
break;
|
||||
case "parallelogram_alt":
|
||||
this.print(`${id}[\\"${name}"\\]`);
|
||||
break;
|
||||
case "trapezoid":
|
||||
this.print(`${id}[/"${name}"\\]`);
|
||||
break;
|
||||
case "trapezoid_alt":
|
||||
this.print(`${id}[\\"${name}"/]`);
|
||||
break;
|
||||
case "double_circle":
|
||||
this.print(`${id}((("${name}")))`);
|
||||
break;
|
||||
}
|
||||
|
||||
if (node.link) {
|
||||
this.print(`click ${id} href "${node.link}" "Click to open workflow"`);
|
||||
}
|
||||
}
|
||||
|
||||
public edge(source: Node, destination: Node, text?: string): void {
|
||||
const sourceId = this.idGenerator.get(source.id);
|
||||
const destinationId = this.idGenerator.get(destination.id);
|
||||
if (text) {
|
||||
this.print(`${sourceId}-- ${text} -->${destinationId}`);
|
||||
} else {
|
||||
this.print(`${sourceId} --> ${destinationId}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function debugGraph(name: string, graph: Graph<any>): void {
|
||||
console.log("```");
|
||||
console.log(`## ${name}`);
|
||||
console.log(new Map(graph.nodes));
|
||||
console.log(graph.edges.map((edge) => ({ source: edge[0].id, destination: edge[1].id, text: edge[2] })));
|
||||
console.log("```");
|
||||
console.log("");
|
||||
}
|
||||
|
||||
export default async function main(dirs: string[], on?: string[], print = false, debug = false): Promise<string> {
|
||||
// Data ingest
|
||||
for (const projectPath of dirs) {
|
||||
const {
|
||||
name,
|
||||
repository: { url },
|
||||
} = await readJson<{ name: string; repository: { url: string } }>(projectPath, "package.json");
|
||||
const workflowsPath = path.join(projectPath, ".github", "workflows");
|
||||
|
||||
let files: string[];
|
||||
try {
|
||||
files = await fs.readdir(workflowsPath);
|
||||
} catch (e) {
|
||||
console.error(`Could not read ${workflowsPath}`, e);
|
||||
continue;
|
||||
}
|
||||
|
||||
const project: Project = {
|
||||
name,
|
||||
url,
|
||||
path: projectPath,
|
||||
workflows: new Map(),
|
||||
};
|
||||
|
||||
for (const file of files.filter((f) => f.endsWith(".yml") || f.endsWith(".yaml"))) {
|
||||
const data = await readYaml<WorkflowYaml>(workflowsPath, file);
|
||||
const name = data.name ?? file;
|
||||
const workflow: Workflow = {
|
||||
id: `${project.name}/${name}`,
|
||||
name,
|
||||
shape: "hexagon",
|
||||
path: path.join(workflowsPath, file),
|
||||
project,
|
||||
link: `${project.url}/blob/develop/.github/workflows/${file}`,
|
||||
|
||||
on: data.on,
|
||||
jobs: [],
|
||||
};
|
||||
|
||||
for (const jobId in data.jobs) {
|
||||
const job = data.jobs[jobId];
|
||||
workflow.jobs.push({
|
||||
id: `${workflow.name}/${jobId}`,
|
||||
jobId,
|
||||
name: job.name ?? jobId,
|
||||
strategy: job.strategy,
|
||||
needs: job.needs ? toArray(job.needs) : undefined,
|
||||
shape: "subroutine",
|
||||
link: `${project.url}/blob/develop/.github/workflows/${file}`,
|
||||
});
|
||||
}
|
||||
|
||||
project.workflows.set(name, workflow);
|
||||
workflows.set(name, workflow);
|
||||
}
|
||||
|
||||
projects.set(name, project);
|
||||
}
|
||||
|
||||
const graph = new Graph<Workflow | Node>();
|
||||
for (const workflow of workflows.values()) {
|
||||
if (on && !on.some((trigger) => trigger in workflow.on)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
graph.addNode(workflow);
|
||||
Object.keys(workflow.on).forEach((trigger) => {
|
||||
const nodes = getTriggerNodes(trigger as keyof WorkflowYaml["on"], workflow, on);
|
||||
nodes.forEach((node) => {
|
||||
graph.addNode(node);
|
||||
graph.addEdge(node, workflow, "project" in node ? "workflow_run" : undefined);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// TODO separate disconnected nodes into their own graph
|
||||
graph.cull();
|
||||
|
||||
// This is an awful hack to make the output graphs much better by allowing the splitting of certain nodes //
|
||||
const bifurcatedNodes = [triggers.get("on:workflow_dispatch")].filter(Boolean) as Node[];
|
||||
const removedEdgeMap = new Map<Node, Edge<any>[]>();
|
||||
for (const node of bifurcatedNodes) {
|
||||
removedEdgeMap.set(node, graph.removeNode(node));
|
||||
}
|
||||
|
||||
const components = graph.components;
|
||||
for (const node of bifurcatedNodes) {
|
||||
const removedEdges = removedEdgeMap.get(node)!;
|
||||
components.forEach((graph) => {
|
||||
removedEdges.forEach((edge) => {
|
||||
if (graph.nodes.has(edge[1].id)) {
|
||||
graph.addNode(node);
|
||||
graph.addEdge(...edge);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
if (debug) {
|
||||
debugGraph("global", graph);
|
||||
}
|
||||
|
||||
let text = "";
|
||||
components.forEach((graph) => {
|
||||
const title = [...graph.roots]
|
||||
.map((root) => root.name)
|
||||
.join(" & ")
|
||||
.replaceAll("<br>", " ");
|
||||
const printer = new MermaidFlowchartPrinter("LR", title, true);
|
||||
graph.nodes.forEach((node) => {
|
||||
if ("project" in node) {
|
||||
// TODO unsure about this edge
|
||||
// if (node.jobs.length === 1) {
|
||||
// printer.node(node);
|
||||
// return;
|
||||
// }
|
||||
|
||||
// TODO handle job.if on github.event_name
|
||||
|
||||
const subgraph = new Graph<Job>();
|
||||
for (const job of node.jobs) {
|
||||
subgraph.addNode(job);
|
||||
if (job.needs) {
|
||||
toArray(job.needs).forEach((req) => {
|
||||
subgraph.addEdge(node.jobs.find((job) => job.jobId === req)!, job, "needs");
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
printer.subgraph(node.id, node.name, () => {
|
||||
subgraph.edges.forEach(([source, destination, text]) => {
|
||||
printer.edge(source, destination, text);
|
||||
});
|
||||
|
||||
subgraph.nodes.forEach((job) => {
|
||||
if (!job.strategy?.matrix) {
|
||||
printer.node(job);
|
||||
return;
|
||||
}
|
||||
|
||||
let variations = cartesianProduct(
|
||||
Object.keys(job.strategy.matrix)
|
||||
.filter(
|
||||
(key) =>
|
||||
key !== "include" &&
|
||||
key !== "exclude" &&
|
||||
Array.isArray(job.strategy!.matrix[key]),
|
||||
)
|
||||
.map((matrixKey) => {
|
||||
return job.strategy!.matrix[matrixKey].map((value) => ({ [matrixKey]: value }));
|
||||
}),
|
||||
)
|
||||
.map((variation) => Object.assign({}, ...variation))
|
||||
.filter((variation) => Object.keys(variation).length > 0);
|
||||
|
||||
if (job.strategy.matrix.include) {
|
||||
variations.push(...job.strategy.matrix.include);
|
||||
}
|
||||
job.strategy.matrix.exclude?.forEach((exclusion) => {
|
||||
variations = variations.filter((variation) => {
|
||||
return !shallowCompare(exclusion, variation);
|
||||
});
|
||||
});
|
||||
|
||||
// TODO validate edge case
|
||||
if (variations.length === 0) {
|
||||
printer.node(job);
|
||||
return;
|
||||
}
|
||||
|
||||
const jobName = job.name.replace(/\${{.+}}/g, "").replace(/(?:\(\)| )+/g, " ");
|
||||
printer.subgraph(job.id, jobName, () => {
|
||||
variations.forEach((variation, i) => {
|
||||
let variationName = job.name;
|
||||
if (variationName.includes("${{ matrix.")) {
|
||||
Object.keys(variation).map((key) => {
|
||||
variationName = variationName.replace(`\${{ matrix.${key} }}`, variation[key]);
|
||||
});
|
||||
} else {
|
||||
variationName = `${variationName} (${Object.values(variation).join(", ")})`;
|
||||
}
|
||||
|
||||
printer.node({ ...job, id: `${job.id}-variation-${i}`, name: variationName });
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
printer.node(node);
|
||||
});
|
||||
graph.edges.forEach(([sourceName, destinationName, text]) => {
|
||||
printer.edge(sourceName, destinationName, text);
|
||||
});
|
||||
text += printer.finish(print);
|
||||
|
||||
if (debug) {
|
||||
printer.idGenerator.debug();
|
||||
debugGraph("subgraph", graph);
|
||||
}
|
||||
});
|
||||
|
||||
return text;
|
||||
}
|
||||
|
||||
if (import.meta.url.startsWith("file:")) {
|
||||
const modulePath = url.fileURLToPath(import.meta.url);
|
||||
if (process.argv[1] === modulePath) {
|
||||
const argv = parseArgs<{
|
||||
debug: boolean;
|
||||
on: string | string[];
|
||||
}>(process.argv.slice(2), {
|
||||
string: ["on"],
|
||||
boolean: ["debug"],
|
||||
});
|
||||
|
||||
const on = typeof argv.on === "string" || Array.isArray(argv.on) ? toArray(argv.on) : undefined;
|
||||
main(argv._, on, true, argv.debug)
|
||||
.then((ret) => {
|
||||
process.exit(0);
|
||||
})
|
||||
.catch((e) => {
|
||||
console.error(e);
|
||||
process.exit(1);
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
# Echoes a version based on the git hashes of the element-web & js-sdk checkouts, for the case where
|
||||
# these dependencies are git checkouts.
|
||||
|
||||
set -e
|
||||
|
||||
# Since the deps are fetched from git & linked, we can rev-parse
|
||||
JSSDK_SHA=$(git -C $(pnpm -w root)/matrix-js-sdk rev-parse --short=12 HEAD)
|
||||
VECTOR_SHA=$(git rev-parse --short=12 HEAD) # use the ACTUAL SHA rather than assume develop
|
||||
echo "$VECTOR_SHA-js-$JSSDK_SHA"
|
||||
@@ -0,0 +1,136 @@
|
||||
#!/usr/bin/env perl
|
||||
|
||||
use warnings;
|
||||
use strict;
|
||||
|
||||
use Net::GitHub;
|
||||
use Time::Moment;
|
||||
use Term::ReadPassword;
|
||||
|
||||
# This version of the script emits the cumulative number of bugs, split into open & closed
|
||||
# suitable for drawing the 'top' and 'bottom' of a burndown graph.
|
||||
#
|
||||
# N.B. this doesn't take into account issues changing priority over time, but only their most recent priority.
|
||||
#
|
||||
# If you want instead the number of open issues on a given day, then look at issues-no-state.pl
|
||||
|
||||
my $gh = Net::GitHub->new(
|
||||
login => 'ara4n', pass => read_password("github password: "),
|
||||
);
|
||||
|
||||
$gh->set_default_user_repo('element-hq', 'element-web');
|
||||
|
||||
#my @issues = $gh->issue->repos_issues({ state => 'all', milestone => 3 });
|
||||
my @issues = $gh->issue->repos_issues({ state => 'all' });
|
||||
while ($gh->issue->has_next_page) {
|
||||
push @issues, $gh->issue->next_page;
|
||||
}
|
||||
|
||||
# we want:
|
||||
# day by day:
|
||||
# split by { open, closed }
|
||||
# split by { bug, feature, neither }
|
||||
# each split by { p1, p2, p3, p4, p5, unprioritised } <- priority
|
||||
# each split by { minor, major, critical, cosmetic, network, no-severity } <- severity
|
||||
# then split (with overlap between the groups) as { total, tag1, tag2, ... }?
|
||||
|
||||
# ...and then all over again split by milestone.
|
||||
|
||||
my $days = {};
|
||||
my $schema = {};
|
||||
my $now = Time::Moment->now;
|
||||
|
||||
foreach my $issue (@issues) {
|
||||
next if ($issue->{pull_request});
|
||||
|
||||
# use Data::Dumper;
|
||||
# print STDERR Dumper($issue);
|
||||
|
||||
my @label_list = map { $_->{name} } @{$issue->{labels}};
|
||||
my $labels = {};
|
||||
$labels->{$_} = 1 foreach (@label_list);
|
||||
$labels->{bug}++ if ($labels->{cosmetic} && !$labels->{bug} && !$labels->{feature});
|
||||
|
||||
my $extract_labels = sub {
|
||||
my $label = undef;
|
||||
foreach (@_) {
|
||||
$label ||= $_ if (delete $labels->{$_});
|
||||
}
|
||||
return $label;
|
||||
};
|
||||
|
||||
my $state = $issue->{state};
|
||||
my $type = &$extract_labels(qw(bug feature)) || "neither";
|
||||
my $priority = &$extract_labels(qw(p1 p2 p3 p4 p5)) || "unprioritised";
|
||||
my $severity = &$extract_labels(qw(minor major critical cosmetic network)) || "no-severity";
|
||||
|
||||
my $start = Time::Moment->from_string($issue->{created_at});
|
||||
|
||||
do {
|
||||
my $ymd = $start->strftime('%F');
|
||||
|
||||
$days->{ $ymd }->{ 'created' }->{ $type }->{ $priority }->{ $severity }->{ total }++;
|
||||
$schema->{ 'created' }->{ $type }->{ $priority }->{ $severity }->{ total }++;
|
||||
foreach (keys %$labels) {
|
||||
$days->{ $ymd }->{ 'created' }->{ $type }->{ $priority }->{ $severity }->{ $_ }++;
|
||||
$schema->{ 'created' }->{ $type }->{ $priority }->{ $severity }->{ $_ }++;
|
||||
}
|
||||
|
||||
$start = $start->plus_days(1);
|
||||
# print STDERR "^";
|
||||
} while ($start->compare($now) < 0);
|
||||
|
||||
if ($state eq 'closed') {
|
||||
my $end = Time::Moment->from_string($issue->{closed_at});
|
||||
do {
|
||||
my $ymd = $end->strftime('%F');
|
||||
|
||||
$days->{ $ymd }->{ 'resolved' }->{ $type }->{ $priority }->{ $severity }->{ total }++;
|
||||
$schema->{ 'resolved' }->{ $type }->{ $priority }->{ $severity }->{ total }++;
|
||||
foreach (keys %$labels) {
|
||||
$days->{ $ymd }->{ 'resolved' }->{ $type }->{ $priority }->{ $severity }->{ $_ }++;
|
||||
$schema->{ 'resolved' }->{ $type }->{ $priority }->{ $severity }->{ $_ }++;
|
||||
}
|
||||
|
||||
$end = $end->plus_days(1);
|
||||
} while ($end->compare($now) < 0);
|
||||
# print STDERR "v";
|
||||
}
|
||||
|
||||
# print STDERR "\n";
|
||||
}
|
||||
|
||||
print "day,";
|
||||
foreach my $state (sort keys %{$schema}) {
|
||||
foreach my $type (grep { /^(bug|feature)$/ } sort keys %{$schema->{$state}}) {
|
||||
foreach my $priority (grep { /^(p1|p2)$/ } sort keys %{$schema->{$state}->{$type}}) {
|
||||
foreach my $severity (sort keys %{$schema->{$state}->{$type}->{$priority}}) {
|
||||
# foreach my $tag (sort keys %{$schema->{$state}->{$type}->{$priority}->{$severity}}) {
|
||||
# print "\"$type\n$priority\n$severity\n$tag\",";
|
||||
# }
|
||||
print "\"$state\n$type\n$priority\n$severity\",";
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
print "\n";
|
||||
|
||||
foreach my $day (sort keys %$days) {
|
||||
print "$day,";
|
||||
foreach my $state (sort keys %{$schema}) {
|
||||
foreach my $type (grep { /^(bug|feature)$/ } sort keys %{$schema->{$state}}) {
|
||||
foreach my $priority (grep { /^(p1|p2)$/ } sort keys %{$schema->{$state}->{$type}}) {
|
||||
foreach my $severity (sort keys %{$schema->{$state}->{$type}->{$priority}}) {
|
||||
# foreach my $tag (sort keys %{$schema->{$state}->{$type}->{$priority}->{$severity}}) {
|
||||
# print $days->{$day}->{$state}->{$type}->{$priority}->{$severity}->{$tag} || 0;
|
||||
# print ",";
|
||||
# }
|
||||
print $days->{$day}->{$state}->{$type}->{$priority}->{$severity}->{total} || 0;
|
||||
print ",";
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
print "\n";
|
||||
}
|
||||
|
||||
@@ -0,0 +1,112 @@
|
||||
#!/usr/bin/env perl
|
||||
|
||||
use warnings;
|
||||
use strict;
|
||||
|
||||
use Net::GitHub;
|
||||
use DateTime;
|
||||
use DateTime::Format::ISO8601;
|
||||
use Term::ReadPassword;
|
||||
|
||||
# This version of the script emits the total number of bugs open on a given day,
|
||||
# split by various tags.
|
||||
#
|
||||
# If you want instead the cumulative number of open & closed issues on a given day,
|
||||
# then look at issues-burndown.pl
|
||||
|
||||
my $gh = Net::GitHub->new(
|
||||
login => 'ara4n', pass => read_password("github password: "),
|
||||
);
|
||||
|
||||
$gh->set_default_user_repo('element-hq', 'element-web');
|
||||
|
||||
#my @issues = $gh->issue->repos_issues({ state => 'all', milestone => 3 });
|
||||
my @issues = $gh->issue->repos_issues({ state => 'all' });
|
||||
while ($gh->issue->has_next_page) {
|
||||
push @issues, $gh->issue->next_page;
|
||||
}
|
||||
|
||||
# we want:
|
||||
# day by day:
|
||||
# split by { open, closed }
|
||||
# split by { bug, feature, neither }
|
||||
# each split by { p1, p2, p3, p4, p5, unprioritised } <- priority
|
||||
# each split by { minor, major, critical, cosmetic, network, no-severity } <- severity
|
||||
# then split (with overlap between the groups) as { total, tag1, tag2, ... }?
|
||||
|
||||
# ...and then all over again split by milestone.
|
||||
|
||||
my $days = {};
|
||||
my $schema = {};
|
||||
my $now = DateTime->now();
|
||||
|
||||
foreach my $issue (@issues) {
|
||||
next if ($issue->{pull_request});
|
||||
|
||||
use Data::Dumper;
|
||||
print STDERR Dumper($issue);
|
||||
|
||||
my @label_list = map { $_->{name} } @{$issue->{labels}};
|
||||
my $labels = {};
|
||||
$labels->{$_} = 1 foreach (@label_list);
|
||||
$labels->{bug}++ if ($labels->{cosmetic} && !$labels->{bug} && !$labels->{feature});
|
||||
|
||||
my $extract_labels = sub {
|
||||
my $label = undef;
|
||||
foreach (@_) {
|
||||
$label ||= $_ if (delete $labels->{$_});
|
||||
}
|
||||
return $label;
|
||||
};
|
||||
|
||||
my $type = &$extract_labels(qw(bug feature)) || "neither";
|
||||
my $priority = &$extract_labels(qw(p1 p2 p3 p4 p5)) || "unprioritised";
|
||||
my $severity = &$extract_labels(qw(minor major critical cosmetic network)) || "no-severity";
|
||||
|
||||
my $start = DateTime::Format::ISO8601->parse_datetime($issue->{created_at});
|
||||
my $end = $issue->{closed_at} ? DateTime::Format::ISO8601->parse_datetime($issue->{closed_at}) : $now;
|
||||
|
||||
do {
|
||||
my $ymd = $start->ymd();
|
||||
|
||||
$days->{ $ymd }->{ $type }->{ $priority }->{ $severity }->{ total }++;
|
||||
$schema->{ $type }->{ $priority }->{ $severity }->{ total }++;
|
||||
foreach (keys %$labels) {
|
||||
$days->{ $ymd }->{ $type }->{ $priority }->{ $severity }->{ $_ }++;
|
||||
$schema->{ $type }->{ $priority }->{ $severity }->{ $_ }++;
|
||||
}
|
||||
|
||||
$start = $start->add(days => 1);
|
||||
} while (DateTime->compare($start, $end) < 0);
|
||||
}
|
||||
|
||||
print "day,";
|
||||
foreach my $type (sort keys %{$schema}) {
|
||||
foreach my $priority (sort keys %{$schema->{$type}}) {
|
||||
foreach my $severity (sort keys %{$schema->{$type}->{$priority}}) {
|
||||
# foreach my $tag (sort keys %{$schema->{$type}->{$priority}->{$severity}}) {
|
||||
# print "\"$type\n$priority\n$severity\n$tag\",";
|
||||
# }
|
||||
print "\"$type\n$priority\n$severity\",";
|
||||
}
|
||||
}
|
||||
}
|
||||
print "\n";
|
||||
|
||||
foreach my $day (sort keys %$days) {
|
||||
print "$day,";
|
||||
foreach my $type (sort keys %{$schema}) {
|
||||
foreach my $priority (sort keys %{$schema->{$type}}) {
|
||||
foreach my $severity (sort keys %{$schema->{$type}->{$priority}}) {
|
||||
# foreach my $tag (sort keys %{$schema->{$type}->{$priority}->{$severity}}) {
|
||||
# print $days->{$day}->{$type}->{$priority}->{$severity}->{$tag} || 0;
|
||||
# print ",";
|
||||
# }
|
||||
print $days->{$day}->{$type}->{$priority}->{$severity}->{total} || 0;
|
||||
print ",";
|
||||
}
|
||||
}
|
||||
}
|
||||
print "\n";
|
||||
}
|
||||
|
||||
@@ -0,0 +1,40 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
set -ex
|
||||
|
||||
# Creates a layered environment with the full repo for the app and SDKs cloned
|
||||
# and linked. This gives an element-web dev environment ready to build with
|
||||
# matching branches of react-sdk's dependencies so that changes can be tested
|
||||
# in element-web.
|
||||
|
||||
# Note that this style is different from the recommended developer setup: this
|
||||
# file nests js-sdk inside element-web, while the local
|
||||
# development setup places them all at the same level. We are nesting them here
|
||||
# because some CI systems do not allow moving to a directory above the checkout
|
||||
# for the primary repo (element-web in this case).
|
||||
|
||||
# Install dependencies
|
||||
pnpm install --frozen-lockfile $@
|
||||
|
||||
# Pass appropriate repo to fetchdep.sh
|
||||
export PR_ORG=element-hq
|
||||
export PR_REPO=element-web
|
||||
|
||||
js_sdk_dep=$(jq -r '.dependencies["matrix-js-sdk"]' < $(pnpm -w root)/../apps/web/package.json)
|
||||
|
||||
# Set up the js-sdk first (unless package.json pins a specific version)
|
||||
if [ "$js_sdk_dep" = "github:matrix-org/matrix-js-sdk#develop" ]; then
|
||||
scripts/fetchdep.sh matrix-org matrix-js-sdk develop
|
||||
|
||||
if [ -n "$JS_SDK_GITHUB_BASE_REF" ]; then
|
||||
git -C matrix-js-sdk fetch --depth 1 origin $JS_SDK_GITHUB_BASE_REF
|
||||
git -C matrix-js-sdk checkout $JS_SDK_GITHUB_BASE_REF
|
||||
fi
|
||||
pnpm -C matrix-js-sdk install --frozen-lockfile --ignore-scripts
|
||||
|
||||
# Link into into element-web & the monorepo
|
||||
pnpm -C apps/web link ./matrix-js-sdk
|
||||
pnpm link ./matrix-js-sdk
|
||||
else
|
||||
echo "Skipping matrix-js-sdk fetch and link as package.json pins $js_sdk_dep"
|
||||
fi
|
||||
@@ -0,0 +1,10 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
set -e
|
||||
|
||||
# If $1 looks like v1.2.3 or v1.2.3-foo, strip the leading v, then print it to stdout
|
||||
if [[ $1 =~ ^v[[:digit:]]+\.[[:digit:]]+\.[[:digit:]]+(-.+)?$ ]]; then
|
||||
echo ${1:1}
|
||||
else
|
||||
echo $1
|
||||
fi
|
||||
@@ -0,0 +1,63 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
/*
|
||||
Copyright 2026 Element Creations Ltd.
|
||||
|
||||
SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial
|
||||
Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
// Utility script to mimic yarn classic `link` behaviour
|
||||
// to enable rapid development of libraries like matrix-js-sdk using symlinks/directory junctions
|
||||
// reads .link-config file for DEPENDENCY=PATH values and removes those dependencies from node_modules,
|
||||
// replacing them with a symlink/directory junction.
|
||||
// This tool is a helpful substitute to `pnpm link` as that modifies the package.json & pnpm-lock.yaml files.
|
||||
|
||||
import * as fs from "node:fs/promises";
|
||||
import { join, dirname } from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
import { execSync } from "node:child_process";
|
||||
|
||||
const __dirname = dirname(fileURLToPath(import.meta.url));
|
||||
const configPath = join(__dirname, "..", ".link-config");
|
||||
const nodeModulesPath = join(__dirname, "..", "node_modules");
|
||||
|
||||
try {
|
||||
if (process.env.PLAYWRIGHT_COMMON_DOCKER) process.exit(0); // Skip in docker env
|
||||
|
||||
const configFile = await fs.readFile(configPath, "utf-8");
|
||||
for (const line of configFile.trim().split("\n")) {
|
||||
if (!line || line.startsWith("#")) continue;
|
||||
const [dependency, path] = line.split("=");
|
||||
const dependencyPath = join(nodeModulesPath, dependency);
|
||||
|
||||
try {
|
||||
const stat = await fs.stat(dependencyPath);
|
||||
if (stat.isSymbolicLink()) {
|
||||
const linkPath = await fs.readlink(dependencyPath);
|
||||
if (linkPath === path) {
|
||||
// already done
|
||||
continue;
|
||||
} else {
|
||||
await fs.unlink(dependencyPath);
|
||||
}
|
||||
} else {
|
||||
await fs.rm(dependencyPath, { recursive: true });
|
||||
}
|
||||
|
||||
console.log(`Linking ${dependency} to ${path}`);
|
||||
await fs.symlink(path, dependencyPath);
|
||||
|
||||
const pkgJson = await fs.readFile(join(path, "package.json"), "utf-8");
|
||||
const pkgManager = JSON.parse(pkgJson)["packageManager"]?.split("@").at(0) ?? "yarn";
|
||||
// pnpm install may have wiped out the `node_modules` dir so we have to restore it
|
||||
execSync(`${pkgManager} install --ignore-scripts --frozen-lockfile`, {
|
||||
cwd: dependencyPath,
|
||||
});
|
||||
} catch (e) {
|
||||
console.error(`Failed to link ${dependency}`, e);
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
// Ignore config file not existing
|
||||
}
|
||||
Reference in New Issue
Block a user