mirror of
https://github.com/callstack/agent-device.git
synced 2026-09-14 20:06:34 +08:00
539e848e0c
* fix(ci): stop ten artifact uploads discarding their hidden paths `actions/upload-artifact` has excluded hidden files and directories by default since v4.4 (this repository pins v4.6.2), and most diagnostics here are written under `.tmp`. Ten upload steps across seven files therefore uploaded nothing from those paths: macos.yml's xcresult bundle, both mutation lanes' reports and shards, replays-nightly's fuzz output, xctest-nightly's results, test-app-build-cache's fixture tarball, and 1874-diagnose's per-iteration logs. Most fail silently, since they pair the omission with `if-no-files-found: warn` or `ignore`. test-app-build-cache sets `error`, so that one does not. A structural guard rather than a shared upload wrapper: the wrapper would be a shallow mirror of the action's options over artifacts with different owners, while the policy question — a hidden path needs the flag — is one rule that belongs in one place. Each workflow still declares its own artifact. test/ci/upload-artifact-hidden-paths.test.ts holds it across every workflow and composite action, and is red if any single flag is dropped. * test(ci): scan every YAML shape GitHub accepts, not just top-level *.yml The guard read `.github/workflows/*.yml` and assumed local actions live one directory deep as `action.yml`. GitHub also reads `.yaml` for both, and local actions nest, so a hidden-path upload in any of those shapes passed the gate. It now walks the `.github` tree recursively for either extension, and a second test plants the three shapes the old scan missed and asserts all three are found — executable rather than a one-off manual check. Red against narrowing the extension, against dropping the recursive walk, and against removing any single real flag. Also drops the action-version note under the comment rule in #2087; the version behaviour belongs in the PR, and the assertion message already says what the omission costs.
78 lines
2.8 KiB
TypeScript
78 lines
2.8 KiB
TypeScript
import fs from 'node:fs';
|
|
import os from 'node:os';
|
|
import path from 'node:path';
|
|
import { expect, test } from 'vitest';
|
|
import { parse } from 'yaml';
|
|
|
|
const repoRoot = path.resolve(import.meta.dirname, '../..');
|
|
|
|
type Step = { uses?: unknown; with?: Record<string, unknown> };
|
|
|
|
function uploadSteps(node: unknown): Step[] {
|
|
if (Array.isArray(node)) return node.flatMap(uploadSteps);
|
|
if (node === null || typeof node !== 'object') return [];
|
|
const record = node as Record<string, unknown>;
|
|
const self =
|
|
typeof record.uses === 'string' && record.uses.startsWith('actions/upload-artifact@')
|
|
? [record as Step]
|
|
: [];
|
|
return [...self, ...Object.values(record).flatMap(uploadSteps)];
|
|
}
|
|
|
|
function isHidden(entry: string): boolean {
|
|
return entry
|
|
.split('/')
|
|
.some((segment) => segment.startsWith('.') && segment !== '.' && segment !== '..');
|
|
}
|
|
|
|
/** Every YAML GitHub reads under a `.github` tree: both extensions, local actions at any depth. */
|
|
function offenders(root: string): string[] {
|
|
return fs
|
|
.readdirSync(root, { recursive: true, encoding: 'utf8' })
|
|
.map((entry) => path.join(root, entry))
|
|
.filter((file) => /\.ya?ml$/.test(file) && fs.statSync(file).isFile())
|
|
.flatMap((file) =>
|
|
uploadSteps(parse(fs.readFileSync(file, 'utf8')))
|
|
.filter((step) => {
|
|
const paths = String(step.with?.path ?? '')
|
|
.split('\n')
|
|
.map((entry) => entry.trim())
|
|
.filter(Boolean);
|
|
return paths.some(isHidden) && step.with?.['include-hidden-files'] !== true;
|
|
})
|
|
.map((step) => `${path.relative(root, file)}: ${step.with?.name ?? '(unnamed)'}`),
|
|
);
|
|
}
|
|
|
|
test('every artifact upload that writes a hidden path opts into hidden files', () => {
|
|
expect(
|
|
offenders(path.join(repoRoot, '.github')),
|
|
'these uploads silently discard their hidden paths',
|
|
).toEqual([]);
|
|
});
|
|
|
|
test('the scan reaches every shape GitHub accepts', () => {
|
|
const root = fs.mkdtempSync(path.join(os.tmpdir(), 'hidden-uploads-'));
|
|
const plant = (file: string, name: string) => {
|
|
fs.mkdirSync(path.dirname(path.join(root, file)), { recursive: true });
|
|
fs.writeFileSync(
|
|
path.join(root, file),
|
|
`jobs:\n j:\n steps:\n - uses: actions/upload-artifact@v4\n` +
|
|
` with:\n name: ${name}\n path: .tmp/out\n`,
|
|
);
|
|
};
|
|
try {
|
|
plant('workflows/dotyaml.yaml', 'yaml-extension');
|
|
plant('actions/nested/inner/action.yaml', 'nested-action');
|
|
plant('actions/shallow/action.yml', 'shallow-action');
|
|
|
|
expect(offenders(root).sort()).toEqual([
|
|
'actions/nested/inner/action.yaml: nested-action',
|
|
'actions/shallow/action.yml: shallow-action',
|
|
'workflows/dotyaml.yaml: yaml-extension',
|
|
]);
|
|
} finally {
|
|
fs.rmSync(root, { recursive: true, force: true });
|
|
}
|
|
});
|