Merge branch 'main' into joshmgross/matrix-context-validation

This commit is contained in:
Josh Gross
2022-12-08 14:03:40 -05:00
26 changed files with 5220 additions and 250 deletions
+61
View File
@@ -0,0 +1,61 @@
# Simple workflow for deploying static content to GitHub Pages
name: Deploy static content to Pages
on:
# Runs on pushes targeting the default branch
push:
branches:
- main
# Allows you to run this workflow manually from the Actions tab
workflow_dispatch:
# Sets permissions of the GITHUB_TOKEN to allow deployment to GitHub Pages
permissions:
contents: read
pages: write
id-token: write
packages: read
# Allow one concurrent deployment
concurrency:
group: "pages"
cancel-in-progress: true
jobs:
# Single deploy job since we're just deploying
deploy:
environment:
name: github-pages
url: ${{ steps.deployment.outputs.page_url }}
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v3
- uses: actions/setup-node@v3
with:
node-version: 16
registry-url: 'https://npm.pkg.github.com'
scope: '@github'
- run: npm ci
env:
NODE_AUTH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- name: build dependencies
run: |
npm run build -ws
- name: copy index
run: |
cp ./public/index.html ./dist/index.html
working-directory: ./browser-playground
- name: Setup Pages
uses: actions/configure-pages@v2
- name: Upload artifact
uses: actions/upload-pages-artifact@v1
with:
path: './browser-playground/dist'
- name: Deploy to GitHub Pages
id: deployment
uses: actions/deploy-pages@v1
+2 -2
View File
@@ -1,6 +1,6 @@
{
"name": "@github/actions-languageserver",
"version": "0.1.39",
"version": "0.1.41",
"description": "Language server for GitHub Actions",
"license": "MIT",
"type": "module",
@@ -38,7 +38,7 @@
"prettier-fix": "prettier --write ."
},
"dependencies": {
"@github/actions-languageservice": "^0.1.39",
"@github/actions-languageservice": "^0.1.41",
"@octokit/rest": "^19.0.5",
"vscode-languageserver": "^8.0.2",
"vscode-languageserver-textdocument": "^1.0.7"
+115
View File
@@ -0,0 +1,115 @@
import { hover } from "@github/actions-languageservice/hover";
import { registerLogger, setLogLevel } from "@github/actions-languageservice/log";
import { validate } from "@github/actions-languageservice/validate";
import {
CompletionItem,
Connection,
Hover,
HoverParams,
InitializeParams,
InitializeResult,
TextDocumentPositionParams,
TextDocuments,
TextDocumentSyncKind
} from "vscode-languageserver";
import { TextDocument } from "vscode-languageserver-textdocument";
import { contextProviders } from "./context-providers";
import { InitializationOptions, RepositoryContext } from "./initializationOptions";
import { onCompletion } from "./on-completion";
import { TTLCache } from "./utils/cache";
import { valueProviders } from "./value-providers";
export function initConnection(connection: Connection) {
const documents: TextDocuments<TextDocument> = new TextDocuments(TextDocument);
let sessionToken: string | undefined;
let repos: RepositoryContext[] = [];
const cache = new TTLCache();
let hasWorkspaceFolderCapability = false;
let hasDiagnosticRelatedInformationCapability = false;
// Register remote console logger with language service
registerLogger(connection.console);
connection.onInitialize((params: InitializeParams) => {
const capabilities = params.capabilities;
hasWorkspaceFolderCapability = !!(capabilities.workspace && !!capabilities.workspace.workspaceFolders);
hasDiagnosticRelatedInformationCapability = !!(
capabilities.textDocument &&
capabilities.textDocument.publishDiagnostics &&
capabilities.textDocument.publishDiagnostics.relatedInformation
);
const options: InitializationOptions = params.initializationOptions;
sessionToken = options.sessionToken;
if (options.repos) {
repos = options.repos;
}
if (options.logLevel !== undefined) {
setLogLevel(options.logLevel);
}
const result: InitializeResult = {
capabilities: {
textDocumentSync: TextDocumentSyncKind.Full,
completionProvider: {
resolveProvider: false,
triggerCharacters: [":", "."]
},
hoverProvider: true
}
};
if (hasWorkspaceFolderCapability) {
result.capabilities.workspace = {
workspaceFolders: {
supported: true
}
};
}
return result;
});
// The content of a text document has changed. This event is emitted
// when the text document first opened or when its content has changed.
documents.onDidChangeContent(change => {
validateTextDocument(change.document);
});
async function validateTextDocument(textDocument: TextDocument): Promise<void> {
const repoContext = repos.find(repo => textDocument.uri.startsWith(repo.workspaceUri));
const result = await validate(
textDocument,
valueProviders(sessionToken, repoContext, cache),
contextProviders(sessionToken, repoContext, cache)
);
connection.sendDiagnostics({uri: textDocument.uri, diagnostics: result});
}
// This handler provides the initial list of the completion items.
connection.onCompletion(async ({position, textDocument}: TextDocumentPositionParams): Promise<CompletionItem[]> => {
return await onCompletion(
position,
documents.get(textDocument.uri)!,
sessionToken,
repos.find(repo => textDocument.uri.startsWith(repo.workspaceUri)),
cache
);
});
connection.onHover(async ({position, textDocument}: HoverParams): Promise<Hover | null> => {
return hover(documents.get(textDocument.uri)!, position);
});
// Make the text document manager listen on the connection
// for open, change and close text document events
documents.listen(connection);
// Listen on the connection
connection.listen();
}
@@ -18,13 +18,14 @@ export function contextProviders(
auth: sessionToken
});
const getContext = async (name: string) => {
const getContext = async (name: string, defaultContext: data.Dictionary | undefined) => {
switch (name) {
case "secrets":
const secrets = await getSecrets(octokit, cache, repo.owner, repo.name);
const secretContext = new data.Dictionary();
secrets.forEach(secret => secretContext.add(secret.value, secret));
return secretContext;
defaultContext = defaultContext || new data.Dictionary();
secrets.forEach(secret => defaultContext!.add(secret.value, new data.StringData("***")));
return defaultContext;
}
};
@@ -2,8 +2,15 @@ import {StringData} from "@github/actions-expressions/data/string";
import {Octokit} from "@octokit/rest";
import {TTLCache} from "../utils/cache";
export function getSecrets(octokit: Octokit, cache: TTLCache, owner: string, name: string): Promise<StringData[]> {
return cache.get(`${owner}/${name}/secrets`, undefined, () => fetchSecrets(octokit, owner, name));
export async function getSecrets(
octokit: Octokit,
cache: TTLCache,
owner: string,
name: string
): Promise<StringData[]> {
const repoSecrets = await cache.get(`${owner}/${name}/secrets`, undefined, () => fetchSecrets(octokit, owner, name));
return repoSecrets;
}
async function fetchSecrets(octokit: Octokit, owner: string, name: string): Promise<StringData[]> {
+4 -110
View File
@@ -1,113 +1,7 @@
import {
CompletionItem,
createConnection,
Hover,
HoverParams,
InitializeParams,
InitializeResult,
TextDocumentPositionParams,
TextDocuments,
TextDocumentSyncKind
} from "vscode-languageserver/node";
import {createConnection} from "vscode-languageserver/node";
import {hover, registerLogger, setLogLevel, validate} from "@github/actions-languageservice";
import {TextDocument} from "vscode-languageserver-textdocument";
import {contextProviders} from "./context-providers";
import {InitializationOptions, RepositoryContext} from "./initializationOptions";
import {onCompletion} from "./on-completion";
import {TTLCache} from "./utils/cache";
import {valueProviders} from "./value-providers";
import {initConnection} from "./connection";
// By default create node connection
const connection = createConnection();
const documents: TextDocuments<TextDocument> = new TextDocuments(TextDocument);
let sessionToken: string | undefined;
let repos: RepositoryContext[] = [];
const cache = new TTLCache();
let hasWorkspaceFolderCapability = false;
let hasDiagnosticRelatedInformationCapability = false;
// Register remote console logger with language service
registerLogger(connection.console);
connection.onInitialize((params: InitializeParams) => {
const capabilities = params.capabilities;
hasWorkspaceFolderCapability = !!(capabilities.workspace && !!capabilities.workspace.workspaceFolders);
hasDiagnosticRelatedInformationCapability = !!(
capabilities.textDocument &&
capabilities.textDocument.publishDiagnostics &&
capabilities.textDocument.publishDiagnostics.relatedInformation
);
const options: InitializationOptions = params.initializationOptions;
sessionToken = options.sessionToken;
if (options.repos) {
repos = options.repos;
}
if (options.logLevel !== undefined) {
setLogLevel(options.logLevel);
}
const result: InitializeResult = {
capabilities: {
textDocumentSync: TextDocumentSyncKind.Full,
completionProvider: {
resolveProvider: false,
triggerCharacters: [":", "."]
},
hoverProvider: true
}
};
if (hasWorkspaceFolderCapability) {
result.capabilities.workspace = {
workspaceFolders: {
supported: true
}
};
}
return result;
});
// The content of a text document has changed. This event is emitted
// when the text document first opened or when its content has changed.
documents.onDidChangeContent(change => {
validateTextDocument(change.document);
});
async function validateTextDocument(textDocument: TextDocument): Promise<void> {
const repoContext = repos.find(repo => textDocument.uri.startsWith(repo.workspaceUri));
const result = await validate(
textDocument,
valueProviders(sessionToken, repoContext, cache),
contextProviders(sessionToken, repoContext, cache)
);
connection.sendDiagnostics({uri: textDocument.uri, diagnostics: result});
}
// This handler provides the initial list of the completion items.
connection.onCompletion(async ({position, textDocument}: TextDocumentPositionParams): Promise<CompletionItem[]> => {
return await onCompletion(
position,
documents.get(textDocument.uri)!,
sessionToken,
repos.find(repo => textDocument.uri.startsWith(repo.workspaceUri)),
cache
);
});
connection.onHover(async ({position, textDocument}: HoverParams): Promise<Hover | null> => {
return hover(documents.get(textDocument.uri)!, position);
});
// Make the text document manager listen on the connection
// for open, change and close text document events
documents.listen(connection);
// Listen on the connection
connection.listen();
initConnection(connection);
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@github/actions-languageservice",
"version": "0.1.39",
"version": "0.1.41",
"description": "Language service for GitHub Actions",
"license": "MIT",
"type": "module",
@@ -1,7 +1,9 @@
import {data} from "@github/actions-expressions";
import {complete, getExpressionInput} from "./complete";
import {ContextProviderConfig} from "./context-providers/config";
import {registerLogger} from "./log";
import {getPositionFromCursor} from "./test-utils/cursor-position";
import {TestLogger} from "./test-utils/logger";
const contextProviderConfig: ContextProviderConfig = {
getContext: async (context: string) => {
@@ -11,17 +13,14 @@ const contextProviderConfig: ContextProviderConfig = {
key: "event",
value: new data.StringData("push")
});
case "secrets":
return new data.Dictionary({
key: "DEPLOY_KEY",
value: new data.StringData("DEPLOY_KEY")
});
}
return undefined;
}
};
registerLogger(new TestLogger());
describe("expressions", () => {
it("input extraction", () => {
const test = (input: string) => {
@@ -146,7 +145,7 @@ jobs:
`;
const result = await complete(...getPositionFromCursor(input), undefined, contextProviderConfig);
expect(result.map(x => x.label)).toEqual(["DEPLOY_KEY"]);
expect(result.map(x => x.label)).toEqual(["GITHUB_TOKEN"]);
});
it("needs context only includes referenced jobs", async () => {
@@ -1,9 +1,13 @@
import {TextEdit} from "vscode-languageserver-types";
import {complete} from "./complete";
import {WorkflowContext} from "./context/workflow-context";
import {registerLogger} from "./log";
import {getPositionFromCursor} from "./test-utils/cursor-position";
import {TestLogger} from "./test-utils/logger";
import {ValueProviderConfig, ValueProviderKind} from "./value-providers/config";
registerLogger(new TestLogger());
describe("completion", () => {
it("runs-on", async () => {
const input = "on: push\njobs:\n build:\n runs-on: |";
@@ -3,7 +3,7 @@ import {Dictionary} from "@github/actions-expressions/data/dictionary";
import {ExpressionData, Pair} from "@github/actions-expressions/data/expressiondata";
export type ContextProviderConfig = {
getContext: (name: string) => Promise<data.Dictionary | undefined>;
getContext: (name: string, defaultContext: data.Dictionary | undefined) => Promise<data.Dictionary | undefined>;
};
/**
@@ -20,17 +20,9 @@ export async function getContext(
const filteredNames = filterContextNames(names, workflowContext);
for (const contextName of filteredNames) {
let value: ContextValue | undefined;
let value = (await getDefaultContext(contextName, workflowContext)) || new data.Dictionary();
value = await getDefaultContext(contextName, workflowContext);
if (!value) {
value = await config?.getContext(contextName);
}
if (!value) {
value = new data.Dictionary();
}
value = (await config?.getContext(contextName, value)) || value;
context.add(contextName, value);
}
@@ -55,6 +47,9 @@ async function getDefaultContext(name: string, workflowContext: WorkflowContext)
case "inputs":
return getInputsContext(workflowContext);
case "secrets":
return objectToDictionary({GITHUB_TOKEN: "***"});
case "steps":
return getStepsContext(workflowContext);
@@ -0,0 +1,19 @@
import {Logger} from "../log";
export class TestLogger implements Logger {
error(message: string): void {
throw new Error(`Error: ${message}`);
}
warn(message: string): void {
console.warn(message);
}
info(message: string): void {
console.info(message);
}
log(message: string): void {
console.warn(message);
}
}
@@ -1,7 +1,11 @@
import {DiagnosticSeverity} from "vscode-languageserver-types";
import {registerLogger} from "./log";
import {createDocument} from "./test-utils/document";
import {TestLogger} from "./test-utils/logger";
import {validate} from "./validate";
registerLogger(new TestLogger());
describe("expression validation", () => {
it("access invalid context field", async () => {
const result = await validate(
+3 -3
View File
@@ -22,6 +22,7 @@ import {ContextProviderConfig} from "./context-providers/config";
import {getContext} from "./context-providers/default";
import {getWorkflowContext, WorkflowContext} from "./context/workflow-context";
import {AccessError, wrapDictionary} from "./expression-validation/error-dictionary";
import {error} from "./log";
import {nullTrace} from "./nulltrace";
import {getAllowedContext} from "./utils/allowed-context";
import {findToken} from "./utils/find-token";
@@ -75,8 +76,7 @@ export async function validate(
});
}
} catch (e) {
// TODO: Handle error here
console.error(e);
error(`Unhandled error while validating: ${e}`);
}
return diagnostics;
@@ -211,7 +211,7 @@ async function validateExpression(
range: mapRange(expression.range)
});
} else {
// Ignore error
throw e;
}
}
}
+9
View File
@@ -0,0 +1,9 @@
## Development
Install dependencies locally, unfortunately via the workspace is not enough to run `webpack-dev-server`. Then
```bash
$ npm start
```
to serve the app at `localhost:8080`.
+35
View File
@@ -0,0 +1,35 @@
{
"name": "browser-playground",
"version": "0.1.41",
"description": "",
"private": true,
"main": "index.js",
"type": "module",
"dependencies": {
"@github/actions-languageserver": "^0.1.41",
"monaco-editor-webpack-plugin": "^7.0.1",
"monaco-editor-workers": "^0.34.2",
"monaco-languageclient": "^4.0.3",
"path-browserify": "^1.0.1",
"vscode-languageserver": "^8.0.2",
"vscode-languageserver-protocol": "^3.17.2"
},
"scripts": {
"build": "webpack --mode production",
"clean": "rimraf dist",
"start": "webpack-dev-server --mode development --open",
"test": "exit 0",
"watch": "webpack --watch --mode development --env esbuild"
},
"license": "MIT",
"devDependencies": {
"css-loader": "^6.7.2",
"rimraf": "^3.0.2",
"style-loader": "^3.3.1",
"ts-loader": "^9.4.2",
"typescript": "^4.9.4",
"webpack": "^5.75.0",
"webpack-cli": "^5.0.1",
"webpack-dev-server": "^4.11.1"
}
}
+15
View File
@@ -0,0 +1,15 @@
<!DOCTYPE html>
<html>
<head>
<title>GitHub Actions Language Services</title>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<script defer type="module" src="./index.js"></script>
</head>
<body>
<div id="container" style="width: 95vw;height: 95vh"></div>
</body>
</html>
+70
View File
@@ -0,0 +1,70 @@
import * as monaco from "monaco-editor";
import {
CloseAction,
ErrorAction,
MessageTransports,
MonacoLanguageClient,
MonacoServices,
} from "monaco-languageclient";
import {
BrowserMessageReader,
BrowserMessageWriter,
} from "vscode-languageserver-protocol/browser.js";
monaco.editor.create(document.getElementById("container")!, {
value: `name: Demo workflow
on:
push:
workflow_dispatch:
inputs:
name:
type: string
jobs:
say-hello:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- run: echo "Hello \${{ github.event.inputs.name }}"`,
language: "yaml",
});
function createLanguageClient(
transports: MessageTransports
): MonacoLanguageClient {
return new MonacoLanguageClient({
name: "GitHub Actions Language Client",
clientOptions: {
// Handle all yaml files as workflows
documentSelector: [{ language: "yaml" }],
errorHandler: {
error: () => ({ action: ErrorAction.Continue }),
closed: () => ({ action: CloseAction.DoNotRestart }),
},
// Custom options for the GitHub Actions language server
initializationOptions: {},
},
connectionProvider: {
get: () => Promise.resolve(transports),
},
});
}
MonacoServices.install();
const worker = new Worker(
new URL("../service-worker/service-worker.ts", import.meta.url),
{
type: "module",
}
);
const reader = new BrowserMessageReader(worker);
const writer = new BrowserMessageWriter(worker);
const languageClient = createLanguageClient({ reader, writer });
languageClient.start();
reader.onClose(() => languageClient.stop());
@@ -0,0 +1,9 @@
{
"extends": "../../tsconfig.json",
"compilerOptions": {
"lib": ["ES2022", "DOM"]
},
"references": [
{ "path": "../service-worker" }
]
}
@@ -0,0 +1,14 @@
import {
BrowserMessageReader,
BrowserMessageWriter,
createConnection,
} from "vscode-languageserver/browser.js";
import { initConnection } from "@github/actions-languageserver/connection";
const messageReader = new BrowserMessageReader(self);
const messageWriter = new BrowserMessageWriter(self);
const connection = createConnection(messageReader, messageWriter);
initConnection(connection);
@@ -0,0 +1,6 @@
{
"extends": "../../tsconfig.json",
"compilerOptions": {
"lib": ["ES2022", "WebWorker"],
}
}
+28
View File
@@ -0,0 +1,28 @@
{
"compilerOptions": {
"target": "ES2022",
"module": "ES2022",
"moduleResolution": "Node16",
"lib": ["ES2022", "dom"],
"esModuleInterop": true,
"resolveJsonModule": true,
"declaration": true,
"declarationMap": true,
"sourceMap": true,
"inlineSources": false,
"stripInternal": true,
"strict": true,
"strictPropertyInitialization": false,
"importHelpers": true,
"downlevelIteration": false,
"noImplicitReturns": true,
"noUnusedParameters": true,
"noUnusedLocals": true,
// Disallow inconsistently-cased references to the same file
"forceConsistentCasingInFileNames": true,
"noImplicitOverride": true,
"rootDir": "src",
"outDir": "dist",
"composite": true
},
}
+59
View File
@@ -0,0 +1,59 @@
const MonacoWebpackPlugin = require("monaco-editor-webpack-plugin");
const path = require("path");
const editorConfig = {
entry: "./src/client/client.ts",
devtool: "source-map",
output: {
path: path.resolve(__dirname, "dist"),
filename: "./index.js",
},
resolve: {
extensions: [".ts", ".js"],
fallback: {
path: require.resolve("path-browserify"),
},
},
module: {
rules: [
{
test: /\.ts$/,
exclude: /node_modules/,
use: [
{
loader: "ts-loader",
options: {
compilerOptions: {
sourceMap: true,
},
},
},
],
},
{
test: /\.m?js$/,
resolve: {
fullySpecified: false, // disable the behaviour
},
},
{
test: /\.css$/,
use: ["style-loader", "css-loader"],
},
{
test: /\.ttf$/,
type: "asset/resource",
},
],
},
devServer: {
static: {
directory: path.join(__dirname, "public"),
},
compress: true,
port: 3000,
},
plugins: [new MonacoWebpackPlugin()],
};
module.exports = [editorConfig]
+1 -1
View File
@@ -1,5 +1,5 @@
{
"$schema": "node_modules/lerna/schemas/lerna-schema.json",
"useWorkspaces": true,
"version": "0.1.39"
"version": "0.1.41"
}
+4734 -109
View File
File diff suppressed because it is too large Load Diff
+2 -1
View File
@@ -2,7 +2,8 @@
"private": true,
"workspaces": [
"actions-languageservice",
"actions-languageserver"
"actions-languageserver",
"browser-playground"
],
"devDependencies": {
"lerna": "^6.0.3"