Skip to content
Closed
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
The table of contents is too big for display.
Diff view
Diff view
  •  
  •  
  •  
38 changes: 38 additions & 0 deletions .github/actions/linux-core-artifact/action.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
name: Checked Linux core artifact
description: Restore the same architecture, commit and version that passed Zig/glibc validation in this run.
inputs:
arch:
description: x64 or arm64 artifact architecture
required: true
rust-target:
description: Expected native GNU Linux target
required: true
probe-version:
description: Expected embedded core version
required: true
runs:
using: composite
steps:
- uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4
with:
name: ci-linux-core-${{ inputs.arch }}-${{ github.sha }}
path: apps/desktop/probe-build
- name: Verify the checked core and restore executable mode
shell: bash
working-directory: apps/desktop
env:
EXPECTED_TARGET: ${{ inputs.rust-target }}
EXPECTED_ARCH: ${{ inputs.arch }}
EXPECTED_VERSION: ${{ inputs.probe-version }}
EXPECTED_REVISION: ${{ github.sha }}
run: |
case "$EXPECTED_ARCH:$EXPECTED_TARGET" in
x64:x86_64-unknown-linux-gnu|arm64:aarch64-unknown-linux-gnu) ;;
*) echo 'Invalid Linux core architecture/target pair' >&2; exit 1 ;;
esac
test "$(cat probe-build/source-revision.txt)" = "$EXPECTED_REVISION"
test "$(cat probe-build/rust-target.txt)" = "$EXPECTED_TARGET"
test "$(cat probe-build/probe-version.txt)" = "$EXPECTED_VERSION"
(cd probe-build && sha256sum --check agent-relay-probe.sha256)
# Artifact downloads deliberately normalize permissions to 0644.
chmod 0755 probe-build/agent-relay-probe
76 changes: 76 additions & 0 deletions .github/actions/rust-lane-cache/action.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
name: Rust lane cache
description: Save parallel build lanes independently and seed a new lane from the old shared cache.
inputs:
legacy-key:
description: Complete former key plus job name (shared-key replaces both)
required: true
lane:
description: Package or tests lane
required: true
discard-profile:
description: Unused Cargo profile to drop from restored seeds (debug, release or none)
required: true
runs:
using: composite
steps:
- name: Restore the lane cache
uses: Swatinem/rust-cache@d1527c8f67ef9af357290475d4af4b0fab8f12e0 # v2
with:
shared-key: ${{ inputs.legacy-key }}-${{ inputs.lane }}
workspaces: apps/desktop/probe
# This marker distinguishes any restored lane cache, including a
# partial lockfile match, from a complete miss. cache-hit alone only
# reports exact matches and cannot safely guard the legacy restore.
cache-directories: ${{ runner.temp }}/relay-rust-cache/${{ inputs.legacy-key }}-${{ inputs.lane }}
- name: Check whether the lane needs its legacy seed
id: seed
shell: bash
env:
CACHE_MARKER: ${{ runner.temp }}/relay-rust-cache/${{ inputs.legacy-key }}-${{ inputs.lane }}/restored
run: |
if [ -f "$CACHE_MARKER" ]; then
echo 'needed=false' >> "$GITHUB_OUTPUT"
else
echo 'needed=true' >> "$GITHUB_OUTPUT"
fi
- name: Seed a new lane from the previous shared cache
if: steps.seed.outputs.needed == 'true'
uses: Swatinem/rust-cache@d1527c8f67ef9af357290475d4af4b0fab8f12e0 # v2
with:
# Match the old cache namespace AND path list. It is read-only:
# parallel lanes must never compete to save this shared entry.
shared-key: ${{ inputs.legacy-key }}
workspaces: apps/desktop/probe
save-if: 'false'
- name: Prune the restored profile this lane never uses
shell: bash
working-directory: apps/desktop
env:
DISCARD_PROFILE: ${{ inputs.discard-profile }}
run: |
# Only known, ignored Cargo build directories may be removed. Never
# accept arbitrary paths or profiles from callers, and reject symlinks.
case "$DISCARD_PROFILE" in
debug|release)
for target_dir in probe/target \
probe/target/aarch64-apple-darwin \
probe/target/x86_64-apple-darwin \
probe/target/aarch64-unknown-linux-gnu \
probe/target/x86_64-unknown-linux-gnu; do
if [ -L "$target_dir" ] || ! git check-ignore --quiet -- "$target_dir/$DISCARD_PROFILE"; then
echo "Refusing to prune an untrusted build directory: $target_dir" >&2
exit 1
fi
rm -rf -- "${target_dir:?}/${DISCARD_PROFILE:?}"
done
;;
none) ;;
*) echo "Unknown discard-profile: $DISCARD_PROFILE" >&2; exit 1 ;;
esac
- name: Mark the lane cache for future restores
shell: bash
env:
CACHE_MARKER: ${{ runner.temp }}/relay-rust-cache/${{ inputs.legacy-key }}-${{ inputs.lane }}/restored
run: |
mkdir -p "$(dirname "$CACHE_MARKER")"
echo 'lane-cache-v1' > "$CACHE_MARKER"
25 changes: 25 additions & 0 deletions .github/actions/swift-cache/action.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
name: Swift package cache
description: Reuse SwiftPM builds within one architecture, lane and installed SDK/toolchain.
inputs:
arch:
description: Runner architecture
required: true
lane:
description: Isolated package or tests build lane
required: true
runs:
using: composite
steps:
- name: Fingerprint Swift and the SDK
id: toolchain
shell: bash
run: |
identity=$( { swift --version; xcodebuild -version; xcrun --show-sdk-version; } | shasum -a 256 | cut -d ' ' -f 1)
echo "identity=$identity" >> "$GITHUB_OUTPUT"
- uses: actions/cache@0057852bfaa89a56745cba8c7296529d2fc39830 # v4
with:
path: apps/desktop/macos/.build
key: swift-v1-${{ inputs.arch }}-${{ inputs.lane }}-${{ steps.toolchain.outputs.identity }}-${{ hashFiles('apps/desktop/macos/Package.swift', 'apps/desktop/macos/Package.resolved') }}-${{ github.sha }}
restore-keys: |
swift-v1-${{ inputs.arch }}-${{ inputs.lane }}-${{ steps.toolchain.outputs.identity }}-${{ hashFiles('apps/desktop/macos/Package.swift', 'apps/desktop/macos/Package.resolved') }}-
swift-v1-${{ inputs.arch }}-${{ inputs.lane }}-${{ steps.toolchain.outputs.identity }}-
9 changes: 9 additions & 0 deletions .github/actions/windows-build-cache-prune/action.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
name: Prune Windows Cargo workspace products
description: Keep reusable dependencies while removing probe binaries and tests before the build cache is saved.
inputs:
profile:
description: Cargo build profile directory (debug or release)
required: true
runs:
using: node20
main: main.cjs
139 changes: 139 additions & 0 deletions .github/actions/windows-build-cache-prune/main.cjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,139 @@
'use strict';

const fs = require('node:fs');
const path = require('node:path');

function inspect(filename) {
try {
return fs.lstatSync(filename);
} catch (error) {
if (error.code === 'ENOENT') return undefined;
throw error;
}
}

function directory(filename) {
const info = inspect(filename);
if (!info) return false;
if (info.isSymbolicLink() || !info.isDirectory()) {
throw new Error(`Refusing an unsafe Cargo build directory: ${filename}`);
}
return true;
}

function packageName(workspace) {
const manifest = path.join(workspace, 'probe', 'Cargo.toml');
const info = inspect(manifest);
if (!info || info.isSymbolicLink() || !info.isFile()) {
throw new Error('Expected a regular probe/Cargo.toml manifest');
}
let inPackage = false;
for (const line of fs.readFileSync(manifest, 'utf8').split(/\r?\n/)) {
if (/^\s*\[/.test(line)) {
inPackage = /^\s*\[package\]\s*(?:#.*)?$/.test(line);
continue;
}
if (!inPackage) continue;
const name = line.match(/^\s*name\s*=\s*(["'])([A-Za-z0-9][A-Za-z0-9_-]*)\1\s*(?:#.*)?$/);
if (name) return name[2];
}
throw new Error('Could not read a safe name from probe/Cargo.toml [package]');
}

function sizeWithoutSymlinks(filename) {
const info = fs.lstatSync(filename);
if (info.isSymbolicLink()) {
throw new Error(`Refusing a symlink in Cargo workspace products: ${filename}`);
}
if (info.isFile()) return info.size;
if (!info.isDirectory()) {
throw new Error(`Refusing a special Cargo workspace product: ${filename}`);
}
return fs
.readdirSync(filename)
.reduce((bytes, name) => bytes + sizeWithoutSymlinks(path.join(filename, name)), 0);
}

function pruneWorkspaceArtifacts({ workspace, profile }) {
if (!['debug', 'release'].includes(profile)) {
throw new Error('Cargo cache profile must be debug or release');
}
if (typeof workspace !== 'string' || !path.isAbsolute(workspace)) {
throw new Error('GITHUB_WORKSPACE must be an absolute repository path');
}
const root = path.resolve(workspace);
if (!directory(root) || !directory(path.join(root, 'probe'))) {
throw new Error('Expected the checked-out probe package');
}
const name = packageName(root);
const summary = { fingerprints: 0, buildDirectories: 0, dependencyFiles: 0, bytes: 0 };
// Validate every parent/container before collecting a deletion plan. Never
// follow a target, profile or artifact-directory symlink outside this tree.
const target = path.join(root, 'probe', 'target');
if (!directory(target)) return summary;
const buildProfile = path.join(target, profile);
if (!directory(buildProfile)) return summary;
const containers = {};
for (const container of ['.fingerprint', 'build', 'deps']) {
const filename = path.join(buildProfile, container);
if (directory(filename)) containers[container] = filename;
}
const escapedName = name.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
const packageHash = new RegExp(`^${escapedName}-([0-9a-f]{8,64})$`);
const hashes = new Set();
const plan = [];
for (const entry of containers['.fingerprint'] ? fs.readdirSync(containers['.fingerprint']) : []) {
const match = entry.match(packageHash);
if (!match) continue;
const filename = path.join(containers['.fingerprint'], entry);
directory(filename);
hashes.add(match[1]);
plan.push({ filename, kind: 'fingerprints' });
}
for (const entry of containers.build ? fs.readdirSync(containers.build) : []) {
if (!packageHash.test(entry)) continue;
const filename = path.join(containers.build, entry);
directory(filename);
plan.push({ filename, kind: 'buildDirectories' });
}
for (const entry of containers.deps ? fs.readdirSync(containers.deps) : []) {
// Cargo puts the target hash immediately before the extension(s), e.g.
// libprobe-HASH.rlib, hosting-HASH.exe or probe-HASH.pdb. The target name
// alone cannot distinguish workspace tests from dependency build tools.
const match = entry.match(/^[^.]+-([0-9a-f]{8,64})(?:\..*)?$/);
if (!match || !hashes.has(match[1])) continue;
const filename = path.join(containers.deps, entry);
const info = fs.lstatSync(filename);
if (info.isSymbolicLink() || !info.isFile()) {
throw new Error(`Refusing an unsafe Cargo workspace artifact: ${filename}`);
}
plan.push({ filename, kind: 'dependencyFiles' });
}
// Validate nested products before the first deletion, including symlinks
// inside workspace build-script outputs. Dependency directories are untouched.
for (const product of plan) summary.bytes += sizeWithoutSymlinks(product.filename);
for (const product of plan) {
fs.rmSync(product.filename, { recursive: true });
summary[product.kind] += 1;
}
return summary;
}

module.exports = { pruneWorkspaceArtifacts };

if (require.main === module) {
try {
const result = pruneWorkspaceArtifacts({
workspace: path.join(process.env.GITHUB_WORKSPACE, 'apps', 'desktop'),
profile: process.env.INPUT_PROFILE,
});
console.log(
`Pruned Cargo workspace products: ${result.fingerprints} fingerprints, ` +
`${result.buildDirectories} build directories, ${result.dependencyFiles} artifact files, ` +
`${result.bytes} bytes. Dependency build scripts and proc-macros are retained.`
);
} catch (error) {
console.error(error.message);
process.exitCode = 1;
}
}
Loading
Loading