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
1 change: 1 addition & 0 deletions .github/CODEOWNERS
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
* @agjs
7 changes: 5 additions & 2 deletions .github/desired-repo-settings.json
Original file line number Diff line number Diff line change
Expand Up @@ -28,10 +28,13 @@
"semgrep SAST",
"analyze (javascript-typescript)"
],
"required_approving_review_count": 0,
"required_approving_review_count": 1,
"dismiss_stale_reviews": true,
"require_code_owner_reviews": true,
"require_last_push_approval": true,
"enforce_admins": true,
"required_linear_history": true,
"required_signatures": true,
"required_signatures": false,
"allow_force_pushes": false,
"allow_deletions": false
}
Expand Down
4 changes: 3 additions & 1 deletion .github/workflows/security-deps.yml
Original file line number Diff line number Diff line change
Expand Up @@ -14,13 +14,15 @@ concurrency:

permissions:
contents: read
security-events: write

jobs:
scan:
name: dep vuln scan (osv + audit)
runs-on: ubuntu-24.04
timeout-minutes: 10
permissions:
contents: read
security-events: write

env:
OSV_SCANNER_VERSION: '2.3.8'
Expand Down
6 changes: 4 additions & 2 deletions .github/workflows/security-sast.yml
Original file line number Diff line number Diff line change
Expand Up @@ -14,14 +14,16 @@ concurrency:

permissions:
contents: read
security-events: write
pull-requests: read

jobs:
semgrep:
name: semgrep SAST
runs-on: ubuntu-24.04
timeout-minutes: 15
permissions:
contents: read
security-events: write
pull-requests: read
container:
image: semgrep/semgrep:1.142.0@sha256:03402a5040a88a570dec58375ef1a19fa777dd61575afdc7d5527ddf308dd765

Expand Down
7 changes: 5 additions & 2 deletions .github/workflows/security-secrets.yml
Original file line number Diff line number Diff line change
Expand Up @@ -14,14 +14,16 @@ concurrency:

permissions:
contents: read
security-events: write
pull-requests: read

jobs:
gitleaks:
name: gitleaks secret scan
runs-on: ubuntu-24.04
timeout-minutes: 10
permissions:
contents: read
security-events: write
pull-requests: read

env:
GITLEAKS_VERSION: '8.30.1'
Expand All @@ -31,6 +33,7 @@ jobs:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
fetch-depth: 0
persist-credentials: false

- name: Install gitleaks CLI
run: |
Expand Down
38 changes: 38 additions & 0 deletions scripts/_lib.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
import { mkdtempSync, readFileSync, rmSync } from 'node:fs';
import { tmpdir } from 'node:os';
import { join } from 'node:path';

import { afterEach, describe, expect, it } from 'vitest';

import { writeFileSafe } from './_lib.js';

describe('writeFileSafe', () => {
const dirs: string[] = [];

afterEach(() => {
for (const dir of dirs.splice(0)) {
rmSync(dir, { recursive: true, force: true });
}
});

it('creates a missing file and refuses to clobber it without overwrite', () => {
const dir = mkdtempSync(join(tmpdir(), 'phaser-write-'));
dirs.push(dir);
const path = join(dir, 'nested', 'out.ts');

expect(writeFileSafe(path, 'one\n')).toBe(true);
expect(readFileSync(path, 'utf8')).toBe('one\n');
expect(writeFileSafe(path, 'two\n')).toBe(false);
expect(readFileSync(path, 'utf8')).toBe('one\n');
});

it('overwrites when overwrite is true', () => {
const dir = mkdtempSync(join(tmpdir(), 'phaser-write-'));
dirs.push(dir);
const path = join(dir, 'out.ts');

expect(writeFileSafe(path, 'one\n')).toBe(true);
expect(writeFileSafe(path, 'two\n', true)).toBe(true);
expect(readFileSync(path, 'utf8')).toBe('two\n');
});
});
21 changes: 16 additions & 5 deletions scripts/_lib.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { mkdirSync, writeFileSync, existsSync } from 'node:fs';
import { mkdirSync, writeFileSync } from 'node:fs';
import { dirname } from 'node:path';

export const toPascalCase = (s: string): string =>
Expand All @@ -17,13 +17,24 @@ export const toKebabCase = (s: string): string =>
.replace(/[\s_]+/g, '-')
.toLowerCase();

export const writeFileSafe = (path: string, contents: string, overwrite = false): boolean => {
if (existsSync(path) && !overwrite) {
console.warn(` skip ${path} (exists — use --force to overwrite)`);
const isAlreadyExistsError = (err: unknown): boolean => {
if (typeof err !== 'object' || err === null || !('code' in err)) {
return false;
}
return typeof err.code === 'string' && err.code === 'EEXIST';
};

export const writeFileSafe = (path: string, contents: string, overwrite = false): boolean => {
mkdirSync(dirname(path), { recursive: true });
writeFileSync(path, contents, 'utf8');
try {
writeFileSync(path, contents, { encoding: 'utf8', flag: overwrite ? 'w' : 'wx' });
} catch (err: unknown) {
if (!overwrite && isAlreadyExistsError(err)) {
console.warn(` skip ${path} (exists — use --force to overwrite)`);
return false;
}
throw err;
}
console.log(` write ${path}`);
return true;
};
Expand Down
30 changes: 27 additions & 3 deletions scripts/audit-repo-settings.sh
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,21 @@ have_reviews=$(echo "$PROT_CURRENT" | jq -r '.required_pull_request_reviews.requ
[[ "$have_reviews" != "$want_reviews" ]] && \
note_protection "required_approving_review_count = $have_reviews (want $want_reviews)"

want_codeowners=$(echo "$PROT_DESIRED" | jq -r '.require_code_owner_reviews // false')
have_codeowners=$(echo "$PROT_CURRENT" | jq -r '.required_pull_request_reviews.require_code_owner_reviews // false')
[[ "$have_codeowners" != "$want_codeowners" ]] && \
note_protection "require_code_owner_reviews = $have_codeowners (want $want_codeowners)"

want_last_push=$(echo "$PROT_DESIRED" | jq -r '.require_last_push_approval // false')
have_last_push=$(echo "$PROT_CURRENT" | jq -r '.required_pull_request_reviews.require_last_push_approval // false')
[[ "$have_last_push" != "$want_last_push" ]] && \
note_protection "require_last_push_approval = $have_last_push (want $want_last_push)"

want_admins=$(echo "$PROT_DESIRED" | jq -r '.enforce_admins // false')
have_admins=$(echo "$PROT_CURRENT" | jq -r '.enforce_admins.enabled // false')
[[ "$have_admins" != "$want_admins" ]] && \
note_protection "enforce_admins = $have_admins (want $want_admins)"

# required_linear_history
want_lin=$(echo "$PROT_DESIRED" | jq -r '.required_linear_history')
have_lin=$(echo "$PROT_CURRENT" | jq -r '.required_linear_history.enabled // false')
Expand All @@ -120,8 +135,16 @@ if [[ "$protection_drift" -eq 1 ]]; then
# Translate desired JSON → PUT body. Reviews count 0 → null (no required reviews).
rev_count=$(echo "$PROT_DESIRED" | jq -r '.required_approving_review_count')
if [[ "$rev_count" -gt 0 ]]; then
prr_block=$(jq -n --argjson n "$rev_count" \
'{required_approving_review_count: $n, dismiss_stale_reviews: true, require_code_owner_reviews: false}')
prr_block=$(jq -n \
--argjson n "$rev_count" \
--argjson owners "$want_codeowners" \
--argjson last "$want_last_push" \
'{
required_approving_review_count: $n,
dismiss_stale_reviews: true,
require_code_owner_reviews: $owners,
require_last_push_approval: $last
}')
else
prr_block='null'
fi
Expand All @@ -137,11 +160,12 @@ if [[ "$protection_drift" -eq 1 ]]; then
--argjson rsc "$rsc_block" \
--argjson prr "$prr_block" \
--argjson lin "$want_lin" \
--argjson admins "$want_admins" \
--argjson fp "$(echo "$PROT_DESIRED" | jq '.allow_force_pushes')" \
--argjson del "$(echo "$PROT_DESIRED" | jq '.allow_deletions')" \
'{
required_status_checks: $rsc,
enforce_admins: false,
enforce_admins: $admins,
required_pull_request_reviews: $prr,
restrictions: null,
required_linear_history: $lin,
Expand Down
2 changes: 1 addition & 1 deletion vitest.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ export default defineConfig({
test: {
globals: true,
environment: 'jsdom',
include: ['src/**/*.test.ts', 'tests/integration/**/*.test.ts'],
include: ['src/**/*.test.ts', 'tests/integration/**/*.test.ts', 'scripts/**/*.test.ts'],
exclude: ['node_modules/**', 'dist/**', 'tests/smoke/**'],
coverage: {
provider: 'v8',
Expand Down