Skip to content
Merged
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
29 changes: 19 additions & 10 deletions .eslintrc.js
Original file line number Diff line number Diff line change
Expand Up @@ -20,22 +20,31 @@ module.exports = {
ecmaVersion: 2020
},
env: {
node: true,
'node': true,
'googleappsscript/googleappsscript': true
},
rules: {
'comma-dangle': ['error', 'never'],
'max-len': ['error', { code: 100 }],
'camelcase': ['error', {
'ignoreDestructuring': true,
'ignoreImports': true,
'allow': ['access_type', 'redirect_uris'],
}],
'max-len': ['error', {code: 100}],
'camelcase': [
'error',
{
ignoreDestructuring: true,
ignoreImports: true,
allow: ['access_type', 'redirect_uris']
}
],
'guard-for-in': 'off',
'no-var': 'off', // ES3
'no-unused-vars': 'off' // functions aren't used.
},
plugins: [
'googleappsscript'
plugins: ['googleappsscript'],
overrides: [
{
files: ['scripts/**/*.js'],
parserOptions: {
sourceType: 'module'
}
}
]
}
};
21 changes: 15 additions & 6 deletions .gemini/settings.json
Original file line number Diff line number Diff line change
@@ -1,8 +1,17 @@
{
"mcpServers": {
"workspace-developer": {
"httpUrl": "https://workspace-developer.goog/mcp",
"trust": true
}
}
"mcpServers": {
"workspace-developer": {
"httpUrl": "https://workspace-developer.goog/mcp",
"trust": true
}
},
"tools": {
"allowed": [
"run_shell_command(pnpm install)",
"run_shell_command(pnpm format)",
"run_shell_command(pnpm lint)",
"run_shell_command(pnpm check)",
"run_shell_command(pnpm test)"
]
}
}
240 changes: 240 additions & 0 deletions .github/scripts/check-gs.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,240 @@
/**
* Copyright 2025 Google LLC
*
* 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.
*/

/// <reference types="node" />

import {
readdirSync,
statSync,
existsSync,
rmSync,
mkdirSync,
copyFileSync,
writeFileSync
} from 'fs';
import {join, relative, dirname, resolve, sep} from 'path';
import {exec} from 'child_process';
import {promisify} from 'util';

const execAsync = promisify(exec);
const TEMP_ROOT = '.tsc_check';

interface Project {
files: string[];
name: string;
path: string;
}

interface CheckResult {
name: string;
success: boolean;
output: string;
}

// Helper to recursively find all files with a specific extension
function findFiles(dir: string, extension: string, fileList: string[] = []): string[] {
const files = readdirSync(dir);
for (const file of files) {
if (file.endsWith('.js')) continue;
const filePath = join(dir, file);
const stat = statSync(filePath);
if (stat.isDirectory()) {
if (file !== 'node_modules' && file !== '.git' && file !== TEMP_ROOT) {
findFiles(filePath, extension, fileList);
}
} else if (file.endsWith(extension)) {
fileList.push(filePath);
}
}
return fileList;
}

// Find all directories containing appsscript.json
function findProjectRoots(rootDir: string): string[] {
return findFiles(rootDir, 'appsscript.json').map((f) => dirname(f));
}

function createProjects(rootDir: string, projectRoots: string[], allGsFiles: string[]): Project[] {
// Holds files that belong to a formal Apps Script project (defined by the presence of appsscript.json).
const projectGroups = new Map<string, string[]>();

// Holds "orphan" files that do not belong to any defined Apps Script project (no appsscript.json found).
const looseGroups = new Map<string, string[]>();

// Initialize project groups
for (const p of projectRoots) {
projectGroups.set(p, []);
}

for (const file of allGsFiles) {
let assigned = false;
let currentDir = dirname(file);

while (currentDir.startsWith(rootDir) && currentDir !== rootDir) {
if (projectGroups.has(currentDir)) {
projectGroups.get(currentDir)?.push(file);
assigned = true;
break;
}
currentDir = dirname(currentDir);
}

if (!assigned) {
const dir = dirname(file);
if (!looseGroups.has(dir)) {
looseGroups.set(dir, []);
}
looseGroups.get(dir)?.push(file);
}
}

const projects: Project[] = [];
projectGroups.forEach((files, dir) => {
if (files.length > 0) {
projects.push({
files,
name: `Project: ${relative(rootDir, dir)}`,
path: relative(rootDir, dir)
});
}
});
looseGroups.forEach((files, dir) => {
if (files.length > 0) {
projects.push({
files,
name: `Loose Project: ${relative(rootDir, dir)}`,
path: relative(rootDir, dir)
});
}
});

return projects;
}

async function checkProject(project: Project, rootDir: string): Promise<CheckResult> {
const projectNameSafe = project.name.replace(/[^a-zA-Z0-9]/g, '_');
const projectTempDir = join(TEMP_ROOT, projectNameSafe);

// Synchronous setup is fine as it's fast and avoids race conditions on mkdir if we were sharing dirs (we aren't)
mkdirSync(projectTempDir, {recursive: true});

for (const file of project.files) {
const fileRelPath = relative(rootDir, file);
const destPath = join(projectTempDir, fileRelPath.replace(/\.gs$/, '.js'));
const destDir = dirname(destPath);
mkdirSync(destDir, {recursive: true});
copyFileSync(file, destPath);
}

const tsConfig = {
extends: '../../tsconfig.json',
compilerOptions: {
noEmit: true,
allowJs: true,
checkJs: true,
typeRoots: [resolve(rootDir, 'node_modules/@types')]
},
include: ['**/*.js']
};

writeFileSync(
join(projectTempDir, 'tsconfig.json'),
JSON.stringify(tsConfig, null, 2)
);

try {
await execAsync(`tsc -p "${projectTempDir}"`, {cwd: rootDir});
return {name: project.name, success: true, output: ''};
} catch (e: any) {
const rawOutput = (e.stdout || '') + (e.stderr || '');

const rewritten = rawOutput.split('\n').map((line: string) => {
if (line.includes(projectTempDir)) {
let newLine = line.split(projectTempDir + sep).pop();
if (!newLine) {
return line;
}
newLine = newLine.replace(/\.js(:|\()/g, '.gs$1');
return newLine;
}
return line;
}).join('\n');

return {name: project.name, success: false, output: rewritten};
}
}

async function main() {
try {
const rootDir = resolve('.');
const searchArg = process.argv[2];

// 1. Discovery
const projectRoots = findProjectRoots(rootDir);
const allGsFiles = findFiles(rootDir, '.gs');

// 2. Grouping
const projects = createProjects(rootDir, projectRoots, allGsFiles);

// 3. Filtering
const projectsToCheck = projects.filter(p => {
return !searchArg || p.path.startsWith(searchArg);
});

if (projectsToCheck.length === 0) {
console.log('No projects found matching the search path.');
return;
}

// 4. Setup
if (existsSync(TEMP_ROOT)) {
rmSync(TEMP_ROOT, {recursive: true, force: true});
}
mkdirSync(TEMP_ROOT);

console.log(`Checking ${projectsToCheck.length} projects in parallel...`);

// 5. Parallel Execution
const results = await Promise.all(projectsToCheck.map(p => checkProject(p, rootDir)));

// 6. Reporting
let hasError = false;
for (const result of results) {
if (!result.success) {
hasError = true;
console.log(`\n--- Failed: ${result.name} ---`);
console.log(result.output);
}
}

if (hasError) {
console.error('\nOne or more checks failed.');
process.exit(1);
} else {
console.log('\nAll checks passed.');
}

} catch (err) {
console.error('Unexpected error:', err);
process.exit(1);
} finally {
if (existsSync(TEMP_ROOT)) {
rmSync(TEMP_ROOT, {recursive: true, force: true});
}
}
}

main();
30 changes: 13 additions & 17 deletions .github/workflows/lint.yml
Original file line number Diff line number Diff line change
Expand Up @@ -13,26 +13,22 @@
# limitations under the License.
---
name: Lint
on: [push, pull_request, workflow_dispatch]
on:
push:
branches:
- main
pull_request:
jobs:
lint:
concurrency:
group: ${{ github.head_ref || github.ref }}
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.sha }}-${{ github.head_ref || github.ref }}
cancel-in-progress: true
runs-on: ubuntu-20.04
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3.0.2
- uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6
- uses: pnpm/action-setup@a7487c7e89a18df4991f7f222e4898a00d66ddda # v4.1.0
- uses: actions/setup-node@a0853c24544627f65ddf259abe73b1d18a591444 # v5
with:
fetch-depth: 0
- uses: github/super-linter/slim@v4.9.4
env:
ERROR_ON_MISSING_EXEC_BIT: true
VALIDATE_JSCPD: false
VALIDATE_JAVASCRIPT_STANDARD: false
VALIDATE_ALL_CODEBASE: ${{ github.event_name == 'push' }}
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- uses: actions/setup-node@v3
with:
node-version: '14'
- run: npm install
- run: npm run lint
cache: "pnpm"
- run: pnpm i
- run: pnpm lint
18 changes: 9 additions & 9 deletions .github/workflows/publish.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -21,13 +21,16 @@ on:
jobs:
publish:
concurrency:
group: ${{ github.head_ref || github.ref }}
cancel-in-progress: false
runs-on: ubuntu-24.04
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.sha }}-${{ github.head_ref || github.ref }}
cancel-in-progress: true
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3.0.2
- uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6
- uses: pnpm/action-setup@a7487c7e89a18df4991f7f222e4898a00d66ddda # v4.1.0
- uses: actions/setup-node@a0853c24544627f65ddf259abe73b1d18a591444 # v5
with:
fetch-depth: 0
cache: "pnpm"
- run: pnpm i
- name: Get changed files
id: changed-files
uses: tj-actions/changed-files@v23.1
Expand All @@ -36,8 +39,5 @@ jobs:
echo "${CLASP_CREDENTIALS}" > "${HOME}/.clasprc.json"
env:
CLASP_CREDENTIALS: ${{secrets.CLASP_CREDENTIALS}}
- uses: actions/setup-node@v3
with:
node-version: '20'
- run: npm install -g @google/clasp
- run: pnpm install -g @google/clasp
- run: ./.github/scripts/clasp_push.sh ${{ steps.changed-files.outputs.all_changed_files }}
Loading
Loading