Upgrade danger setup and add tailwind deps rule

Co-authored-by: Jamie <113370520+jamiebuilds-signal@users.noreply.github.com>
This commit is contained in:
automated-signalandJamie authored and GitHub committed 2026-03-30 19:03:52 -07:00
1 parent 7c86546268
commit d2f4c9b1ee
17 files changed
+465 -1684

No files matched your search

+1 -3
View File
@@ -11,8 +11,6 @@ jobs:
timeout-minutes: 30
steps:
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5
with:
fetch-depth: 0 # fetch all history
- name: Setup pnpm
uses: pnpm/action-setup@41ff72655975bd51cab0327fa583b6e92b6d3061 # v4
- name: Setup node.js
@@ -21,7 +19,7 @@ jobs:
node-version-file: '.nvmrc'
package-manager-cache: false # Avoid cache key clashes
- name: Install danger node_modules
run: cd danger && pnpm install
run: (cd danger && pnpm install --ignore-workspace)
- name: Run DangerJS
run: pnpm run danger:ci
env:
+1 -1
View File
@@ -1695,7 +1695,7 @@
"test/**",
".*rc.js",
"ci.js",
"dangerfile.js",
"dangerfile.mjs",
"preload.wrapper.ts",
"rolldown.config.ts"
],
-24
View File
@@ -6866,30 +6866,6 @@ Signal Desktop makes use of the following open source projects.
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
## endanger
MIT License
Copyright (c) 2020 Jamie Kyle
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
## enhanced-resolve
Copyright JS Foundation and other contributors
+23
View File
@@ -0,0 +1,23 @@
// Copyright 2026 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
// @ts-check
/** @type {typeof import("danger").danger} */
// @ts-expect-error
export const danger = globalThis.danger;
/** @type {typeof import("danger").warn} */
// @ts-expect-error
export const warn = globalThis.warn;
/** @type {typeof import("danger").fail} */
// @ts-expect-error
export const fail = globalThis.fail;
/** @type {typeof import("danger").message} */
// @ts-expect-error
export const message = globalThis.message;
/** @type {typeof import("danger").markdown} */
// @ts-expect-error
export const markdown = globalThis.markdown;
+4 -5
View File
@@ -1,12 +1,11 @@
{
"dependencies": {
"danger": "12.3.4",
"endanger": "7.0.4",
"semver": "7.7.4",
"typescript": "5.6.3"
"danger": "13.0.7",
"js-yaml": "4.1.0",
"semver": "7.7.4"
},
"pnpm": {
"onlyBuiltDependencies": [
"ignoredBuiltDependencies": [
"core-js"
]
}
+165 -860
View File
File diff suppressed because it is too large. Load diff
+7
View File
@@ -0,0 +1,7 @@
// Copyright 2026 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
// @ts-check
import './rules/enforcePackageJsonPinnedDeps.mjs';
import './rules/enforcePnpmLockfileDepsIntegrity.mjs';
import './rules/enforceTailwindDepsMatch.mjs';
-35
View File
@@ -1,35 +0,0 @@
// Copyright 2022 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
import { run } from 'endanger';
import packageJsonVersionsShouldBePinned from './rules/packageJsonVersionsShouldBePinned';
import pnpmLockDepsShouldHaveIntegrity from './rules/pnpmLockDepsShouldHaveIntegrity';
function isGitDeletedError(error: unknown) {
return (
typeof error === 'object' &&
error != null &&
error['exitCode'] === 128 &&
error['command']?.startsWith('git show ')
);
}
async function main() {
try {
await run(
packageJsonVersionsShouldBePinned(),
pnpmLockDepsShouldHaveIntegrity()
);
} catch (error: unknown) {
if (!isGitDeletedError(error)) {
throw error;
}
}
}
// oxlint-disable-next-line promise/prefer-await-to-then
main().catch(error => {
console.error(error);
process.exit(1);
});
@@ -0,0 +1,55 @@
// Copyright 2026 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
// @ts-check
import { fail } from '../danger-exports.mjs';
import { resolve } from 'node:path';
import { readFileSync } from 'node:fs';
import * as semver from 'semver';
const rootDir = resolve(import.meta.dirname, '..', '..');
const pkgPath = resolve(rootDir, 'package.json');
const pkgContents = readFileSync(pkgPath, 'utf8');
const pkgJson = JSON.parse(pkgContents);
const depTypes = [
'dependencies',
'devDependencies',
'peerDependencies',
'optionalDependencies',
];
/**
* @param {string} depSpec
* @returns {boolean}
*/
function isPinnedVersion(depSpec) {
if (depSpec.startsWith('https:')) {
return depSpec.includes('#');
}
/** @type {string} */
let version;
if (depSpec.startsWith('workspace:')) {
version = depSpec.replace(/^workspace:/, '');
} else {
version = depSpec;
}
return semver.valid(version) != null;
}
for (const depType of depTypes) {
const deps = pkgJson[depType];
if (deps == null) {
continue;
}
for (const [depName, depSpec] of Object.entries(deps)) {
if (typeof depSpec === 'string' && !isPinnedVersion(depSpec)) {
fail(
`**Pin package.json versions**\n` +
`All package.json versions should be pinned to a specific version.\n` +
`See ${depName}@${depSpec} in package.json#${depType}.`,
'package.json'
);
}
}
}
@@ -0,0 +1,28 @@
// Copyright 2026 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
// @ts-check
import { fail } from '../danger-exports.mjs';
import { resolve } from 'node:path';
import { readFileSync } from 'node:fs';
import * as YAML from 'js-yaml';
const rootDir = resolve(import.meta.dirname, '..', '..');
const lockPath = resolve(rootDir, 'pnpm-lock.yaml');
const lockContents = readFileSync(lockPath, 'utf8');
/** @type {any} */
const lockYaml = YAML.load(lockContents);
for (const name of Object.keys(lockYaml.packages)) {
const spec = lockYaml.packages[name];
if (spec.resolution?.integrity == null) {
fail(
`**Dependency resolution missing integrity**\n` +
`All dependencies should have a resolution with an integrity field.\n` +
`You may need to override it or provide it manually.\n` +
`\n` +
`See "${name}".`,
'pnpm-lock.yaml'
);
}
}
+61
View File
@@ -0,0 +1,61 @@
// Copyright 2026 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
// @ts-check
import { fail } from '../danger-exports.mjs';
import { resolve } from 'node:path';
import { readFileSync } from 'node:fs';
import * as YAML from 'js-yaml';
const rootDir = resolve(import.meta.dirname, '..', '..');
const pkgPath = resolve(rootDir, 'package.json');
const pkgContents = readFileSync(pkgPath, 'utf8');
const pkgJson = JSON.parse(pkgContents);
const lockPath = resolve(rootDir, 'pnpm-lock.yaml');
const lockContents = readFileSync(lockPath, 'utf8');
/** @type {any} */
const lockYaml = YAML.load(lockContents);
const expectedVersion = pkgJson.devDependencies.tailwindcss;
if (typeof expectedVersion !== 'string') {
throw new TypeError('Missing tailwindcss package version');
}
/**
* @param {string} pkgName
* @returns {boolean}
*/
function isTailwindPackage(pkgName) {
return pkgName === 'tailwindcss' || pkgName.startsWith('@tailwindcss/');
}
for (const depType of ['dependencies', 'devDependencies']) {
for (const [depName, depSpec] of Object.entries(pkgJson[depType])) {
if (isTailwindPackage(depName) && depSpec !== expectedVersion) {
fail(
`**Tailwind package versions must all match**\n` +
`Expected to match tailwindcss@${expectedVersion}\n` +
`See ${depName}@${depSpec} in package.json#${depType}.`,
'package.json'
);
}
}
}
for (const depKey of Object.keys(lockYaml.packages)) {
const match = depKey.match(/^((?:@[^\/]+\/)?[^@]+)@(.+)$/);
if (match == null) {
throw new Error(`Could not parse "${depKey}"`);
}
const [, depName, depSpec] = match;
if (isTailwindPackage(depName) && depSpec !== expectedVersion) {
fail(
`**Tailwind package versions must all match**\n` +
`Expected to match tailwindcss@${expectedVersion}\n` +
`See ${depName}@${depSpec} in pnpm-lock.yaml#packages.`,
'pnpm-lock.yaml'
);
}
}
@@ -1,88 +0,0 @@
// Copyright 2022 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
import type { File } from 'endanger';
import { Rule } from 'endanger';
import semver from 'semver';
function isPinnedVersion(spec: string): boolean {
if (spec.startsWith('https:')) {
return spec.includes('#');
}
let version: string;
if (spec.startsWith('workspace:')) {
version = spec.replace(/^workspace:/, '');
} else {
version = spec;
}
return semver.valid(version) != null;
}
async function getLineContaining(file: File, text: string) {
const lines = await file.lines();
for (const line of lines) {
// oxlint-disable-next-line no-await-in-loop
if (await line.contains(text)) {
return line;
}
}
return null;
}
const dependencyTypes = [
'dependencies',
'devDependencies',
'peerDependencies',
'optionalDependencies',
];
// oxlint-disable-next-line typescript/no-explicit-any
export default function packageJsonVersionsShouldBePinned(): Rule<any, any> {
return new Rule({
match: {
files: ['**/package.json', '!**/node_modules/**'],
},
messages: {
packageJsonVersionsShouldBePinned: `
**Pin package.json versions**
All package.json versions should be pinned to a specific version.
See {depName}@{depVersion} in {filePath}#{dependencyType}.
`,
},
async run({ files, context }) {
for (const file of files.modifiedOrCreated) {
// oxlint-disable-next-line no-await-in-loop
const pkg = await file.json();
for (const dependencyType of dependencyTypes) {
const deps = pkg[dependencyType];
if (deps == null) {
continue;
}
for (const depName of Object.keys(deps)) {
const depVersion = deps[depName];
if (!isPinnedVersion(depVersion)) {
// oxlint-disable-next-line no-await-in-loop
const line = await getLineContaining(
file,
`"${depName}": "${depVersion}"`
);
context.warn(
'packageJsonVersionsShouldBePinned',
{
file,
line: line ?? undefined,
},
{
depName,
depVersion,
filePath: file.path,
dependencyType,
}
);
}
}
}
}
},
});
}
@@ -1,66 +0,0 @@
// Copyright 2022 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
import { Rule } from 'endanger';
function assert(condition: boolean, message: string): asserts condition {
if (!condition) {
throw new Error(message);
}
}
function isObject(value: unknown): value is object {
return typeof value === 'object' && value != null;
}
// oxlint-disable-next-line typescript/no-explicit-any
function has<T extends object, const K extends T[any]>(
value: T,
key: K
): value is T & Record<K, T[K]> {
return Object.hasOwn(value, key);
}
// oxlint-disable-next-line typescript/no-explicit-any
export default function pnpmLockDepsShouldHaveIntegrity(): Rule<any, any> {
return new Rule({
match: {
files: ['pnpm-lock.yaml'],
},
messages: {
missingIntegrity: `
**Dependency resolution missing integrity**
All dependencies should have a resolution with an integrity field.
You may need to override it or provide it manually.
See "{name}".
`,
},
async run({ files, context }) {
for (const file of files.modifiedOrCreated) {
// oxlint-disable-next-line no-await-in-loop
const contents: unknown = await file.yaml();
assert(
isObject(contents) &&
has(contents, 'packages') &&
isObject(contents.packages),
'pnpm.yaml should be object'
);
for (const [name, spec] of Object.entries(contents.packages)) {
assert(
isObject(spec) &&
has(spec, 'resolution') &&
isObject(spec.resolution),
`${name} spec should be object`
);
if (!has(spec.resolution, 'integrity')) {
context.fail('missingIntegrity', { file }, { name });
}
}
}
},
});
}
-8
View File
@@ -1,8 +0,0 @@
// Copyright 2022 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
// Ensuring that the root directory is the same as this file before we load any
// danger code. This is needed so we can run danger with the danger/package.json
// file in CI
process.chdir(__dirname);
require('./danger/rules.ts');
+5
View File
@@ -0,0 +1,5 @@
// Copyright 2026 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
// @ts-check
import './danger/rules.mjs';
+1 -2
View File
@@ -251,7 +251,7 @@
"cross-env": "7.0.3",
"css-loader": "7.1.2",
"csv-parse": "5.5.6",
"danger": "12.3.3",
"danger": "13.0.7",
"dashdash": "2.0.0",
"debug": "4.3.7",
"direction": "1.0.4",
@@ -261,7 +261,6 @@
"electron-mocha": "13.0.1",
"emoji-regex": "10.4.0",
"encoding": "0.1.13",
"endanger": "7.0.4",
"enhanced-resolve": "5.18.3",
"enquirer": "2.4.1",
"eslint": "10.1.0",
+114 -592
View File
File diff suppressed because it is too large. Load diff