mirror of
https://github.com/garrytan/gstack.git
synced 2026-09-21 04:10:47 +02:00
feat: componentize GStack 2 runtime and release integrity
This commit is contained in:
@@ -0,0 +1,87 @@
|
||||
#!/usr/bin/env node
|
||||
import fs from "node:fs/promises";
|
||||
import path from "node:path";
|
||||
|
||||
const [directory, repository = process.env.GITHUB_REPOSITORY, version = "2.0.0"] = process.argv.slice(2);
|
||||
if (!directory || !repository) {
|
||||
console.error("Usage: create-runtime-release-manifest.mjs <artifact-dir> <owner/repo> [version]");
|
||||
process.exit(2);
|
||||
}
|
||||
|
||||
const targets = [
|
||||
"darwin-arm64",
|
||||
"darwin-x64",
|
||||
"linux-arm64",
|
||||
"linux-x64",
|
||||
"windows-arm64",
|
||||
"windows-x64",
|
||||
];
|
||||
const componentDependencies = {
|
||||
core: [],
|
||||
"browser-code": ["core"],
|
||||
"browser-headless": ["browser-code"],
|
||||
"browser-visible": ["browser-code"],
|
||||
design: ["core"],
|
||||
diagram: ["browser-headless"],
|
||||
pdf: ["diagram"],
|
||||
ios: ["core"],
|
||||
};
|
||||
const capabilityComponents = {
|
||||
browser: ["browser-code", "browser-headless"],
|
||||
"browser-visible": ["browser-code", "browser-visible"],
|
||||
design: ["design"],
|
||||
diagram: ["diagram"],
|
||||
pdf: ["pdf"],
|
||||
ios: ["ios"],
|
||||
};
|
||||
const commonComponents = ["core", "browser-code", "browser-headless", "browser-visible", "design", "diagram", "pdf"];
|
||||
const release = `https://github.com/${repository}/releases/download/v${version}`;
|
||||
const certificateIdentity = `https://github.com/${repository}/.github/workflows/release-artifacts.yml@refs/tags/v${version}`;
|
||||
const targetRecords = {};
|
||||
|
||||
for (const target of targets) {
|
||||
const ids = [...commonComponents, ...(target.startsWith("darwin-") ? ["ios"] : [])];
|
||||
const components = {};
|
||||
for (const component of ids) {
|
||||
const name = `gstack-runtime-${version}-${target}-${component}.tar.gz`;
|
||||
const archive = path.join(directory, name);
|
||||
const bundle = `${archive}.sigstore.json`;
|
||||
const digestFile = `${archive}.sha256`;
|
||||
const [stat, digest, bundleStat] = await Promise.all([
|
||||
fs.stat(archive),
|
||||
fs.readFile(digestFile, "utf8"),
|
||||
fs.stat(bundle),
|
||||
]);
|
||||
const sha256 = digest.trim().split(/\s+/)[0];
|
||||
if (!stat.isFile() || stat.size <= 0 || stat.size > 2 * 1024 * 1024 * 1024) {
|
||||
throw new Error(`Invalid artifact size for ${name}: ${stat.size}`);
|
||||
}
|
||||
if (!bundleStat.isFile() || bundleStat.size <= 0) throw new Error(`Missing Sigstore bundle for ${name}`);
|
||||
if (!/^[a-f0-9]{64}$/.test(sha256)) throw new Error(`Invalid SHA-256 for ${name}`);
|
||||
components[component] = {
|
||||
url: `${release}/${name}`,
|
||||
sha256,
|
||||
bytes: stat.size,
|
||||
format: "tar.gz",
|
||||
root: "gstack",
|
||||
cosignBundleUrl: `${release}/${name}.sigstore.json`,
|
||||
certificateIdentity,
|
||||
certificateOidcIssuer: "https://token.actions.githubusercontent.com",
|
||||
};
|
||||
}
|
||||
targetRecords[target] = { components };
|
||||
}
|
||||
|
||||
const manifest = {
|
||||
schemaVersion: 2,
|
||||
version,
|
||||
skillApi: "2.0",
|
||||
capabilityComponents,
|
||||
componentDependencies,
|
||||
targets: targetRecords,
|
||||
};
|
||||
await fs.writeFile(
|
||||
path.join(directory, "gstack-runtime-manifest.json"),
|
||||
`${JSON.stringify(manifest, null, 2)}\n`,
|
||||
{ flag: "wx", mode: 0o644 },
|
||||
);
|
||||
@@ -0,0 +1,31 @@
|
||||
#!/usr/bin/env node
|
||||
import { spawn } from "node:child_process";
|
||||
|
||||
const child = spawn("bun", [
|
||||
"bin/gstack-redact",
|
||||
"--repo-visibility", "public",
|
||||
"--json",
|
||||
"--max-bytes", "16000000",
|
||||
], { shell: false, windowsHide: true, stdio: ["pipe", "pipe", "inherit"] });
|
||||
let diff = "";
|
||||
process.stdin.setEncoding("utf8");
|
||||
process.stdin.on("data", (chunk) => { diff += chunk; });
|
||||
process.stdin.once("end", () => {
|
||||
const additions = diff
|
||||
.split(/\r?\n/)
|
||||
.filter((line) => line.startsWith("+") && !line.startsWith("+++"))
|
||||
.map((line) => line.slice(1))
|
||||
.join("\n");
|
||||
child.stdin.end(additions);
|
||||
});
|
||||
let stdout = "";
|
||||
child.stdout.setEncoding("utf8");
|
||||
child.stdout.on("data", (chunk) => { stdout += chunk; });
|
||||
child.once("error", (error) => { throw error; });
|
||||
child.once("close", (code) => {
|
||||
const report = JSON.parse(stdout);
|
||||
const high = Number(report.counts?.HIGH ?? 0);
|
||||
const medium = Number(report.counts?.MEDIUM ?? 0);
|
||||
console.log(`credential scan: ${high} high, ${medium} advisory`);
|
||||
process.exitCode = high > 0 || report.oversize || ![0, 2, 3].includes(code) ? 1 : 0;
|
||||
});
|
||||
@@ -0,0 +1,63 @@
|
||||
#!/usr/bin/env node
|
||||
import fs from "node:fs/promises";
|
||||
import os from "node:os";
|
||||
import path from "node:path";
|
||||
import { spawn } from "node:child_process";
|
||||
|
||||
const root = path.resolve(process.argv[2] ?? process.cwd());
|
||||
const temporary = await fs.mkdtemp(path.join(os.tmpdir(), "gstack-npm-smoke-"));
|
||||
try {
|
||||
const pack = await run("npm", ["pack", "--json", "--ignore-scripts", "--pack-destination", temporary], root);
|
||||
const [metadata] = JSON.parse(pack.stdout);
|
||||
if (!metadata?.filename || metadata.size > 2_000_000 || metadata.entryCount > 80) {
|
||||
throw new Error(`Unexpected npm package shape: ${JSON.stringify(metadata)}`);
|
||||
}
|
||||
const archive = path.join(temporary, metadata.filename);
|
||||
const project = path.join(temporary, "consumer");
|
||||
await fs.mkdir(project);
|
||||
await fs.writeFile(path.join(project, "package.json"), '{"private":true}\n');
|
||||
await run("npm", ["install", "--ignore-scripts", "--no-audit", "--no-fund", "--package-lock=false", archive], project);
|
||||
|
||||
const installed = path.join(project, "node_modules", "gstack");
|
||||
const pkg = JSON.parse(await fs.readFile(path.join(installed, "package.json"), "utf8"));
|
||||
if (pkg.gstack?.packageRole !== "runtime-control") throw new Error("Packed role metadata is missing");
|
||||
for (const excluded of ["skills", "browse", "design", "make-pdf", "setup"]) {
|
||||
if (await exists(path.join(installed, excluded))) throw new Error(`Packed package unexpectedly contains ${excluded}`);
|
||||
}
|
||||
|
||||
const state = path.join(temporary, "state");
|
||||
const env = { ...process.env, GSTACK_HOME: state };
|
||||
const version = await run(process.execPath, [path.join(installed, "bin", "gstack"), "--version"], project, env);
|
||||
if (!version.stdout.includes(`gstack runtime ${pkg.gstack.runtimeVersion}`)) throw new Error("Packed gstack version mismatch");
|
||||
await run(process.execPath, [path.join(installed, "bin", "gstack"), "setup"], project, env);
|
||||
const doctor = await run(process.execPath, [path.join(installed, "bin", "gstack"), "doctor", "--json"], project, env, [0, 1]);
|
||||
const report = JSON.parse(doctor.stdout);
|
||||
if (!Array.isArray(report.checks) || !report.checks.some((check) => check.id === "config" && check.status === "pass")) {
|
||||
throw new Error("Packed setup/doctor did not initialize isolated runtime state");
|
||||
}
|
||||
const help = await run(process.execPath, [path.join(installed, "runtime", "runtime-bootstrap.mjs"), "--help"], project, env);
|
||||
if (!help.stdout.includes("Usage:")) {
|
||||
throw new Error(`Packed runtime bootstrap help is unavailable: ${JSON.stringify(help)}`);
|
||||
}
|
||||
console.log(JSON.stringify({ ok: true, size: metadata.size, entryCount: metadata.entryCount, version: pkg.version }));
|
||||
} finally {
|
||||
await fs.rm(temporary, { recursive: true, force: true });
|
||||
}
|
||||
|
||||
async function exists(target) {
|
||||
return fs.access(target).then(() => true, () => false);
|
||||
}
|
||||
|
||||
function run(command, args, cwd, env = process.env, allowed = [0]) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const child = spawn(command, args, { cwd, env, shell: false, windowsHide: true, stdio: ["ignore", "pipe", "pipe"] });
|
||||
let stdout = "";
|
||||
let stderr = "";
|
||||
child.stdout.setEncoding("utf8");
|
||||
child.stderr.setEncoding("utf8");
|
||||
child.stdout.on("data", (chunk) => { stdout += chunk; });
|
||||
child.stderr.on("data", (chunk) => { stderr += chunk; });
|
||||
child.once("error", reject);
|
||||
child.once("close", (code) => allowed.includes(code) ? resolve({ stdout, stderr, code }) : reject(new Error(`${command} ${args.join(" ")} failed (${code})\n${stderr}`)));
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,82 @@
|
||||
#!/usr/bin/env node
|
||||
import { createHash } from "node:crypto";
|
||||
import { constants as fsConstants, createReadStream } from "node:fs";
|
||||
import fs from "node:fs/promises";
|
||||
import path from "node:path";
|
||||
import {
|
||||
RUNTIME_COMPONENT_DEPENDENCIES,
|
||||
runtimeReleaseComponentForPath,
|
||||
} from "../../runtime/install.js";
|
||||
|
||||
const [activeInput, outputInput] = process.argv.slice(2);
|
||||
if (!activeInput || !outputInput) {
|
||||
console.error("Usage: stage-runtime-components.mjs <active-runtime> <output-dir>");
|
||||
process.exit(2);
|
||||
}
|
||||
|
||||
const active = await fs.realpath(path.resolve(activeInput));
|
||||
const output = path.resolve(outputInput);
|
||||
const manifest = JSON.parse(await fs.readFile(path.join(active, ".gstack-bundle.json"), "utf8"));
|
||||
if (manifest?.schemaVersion !== 2 || !Array.isArray(manifest.files)) {
|
||||
throw new Error("Active runtime has no supported bundle manifest");
|
||||
}
|
||||
|
||||
const componentIds = Object.keys(RUNTIME_COMPONENT_DEPENDENCIES).sort();
|
||||
const summary = Object.fromEntries(componentIds.map((id) => [id, { files: 0, bytes: 0 }]));
|
||||
for (const id of componentIds) await fs.mkdir(path.join(output, id, "gstack"), { recursive: true, mode: 0o700 });
|
||||
|
||||
for (const file of manifest.files) {
|
||||
if (!file || typeof file.path !== "string" || !/^[a-f0-9]{64}$/.test(file.sha256) ||
|
||||
!Number.isSafeInteger(file.size) || file.size < 0) {
|
||||
throw new Error("Bundle manifest contains an invalid file record");
|
||||
}
|
||||
const relative = normalizeRelative(file.path);
|
||||
const component = runtimeReleaseComponentForPath(relative);
|
||||
if (component == null) continue; // Playwright's local GC bookkeeping is not runtime input.
|
||||
if (!summary[component]) throw new Error(`No release component declared for ${relative}`);
|
||||
const source = safeJoin(active, relative);
|
||||
const stat = await fs.lstat(source);
|
||||
if (!stat.isFile() || stat.isSymbolicLink()) throw new Error(`Unsafe runtime source file: ${relative}`);
|
||||
if (stat.size !== file.size || await sha256File(source) !== file.sha256) {
|
||||
throw new Error(`Runtime file changed before component staging: ${relative}`);
|
||||
}
|
||||
const destination = safeJoin(path.join(output, component, "gstack"), relative);
|
||||
await fs.mkdir(path.dirname(destination), { recursive: true, mode: 0o700 });
|
||||
await fs.copyFile(source, destination, fsConstants.COPYFILE_EXCL);
|
||||
if (process.platform !== "win32") await fs.chmod(destination, stat.mode & 0o777);
|
||||
summary[component].files += 1;
|
||||
summary[component].bytes += stat.size;
|
||||
}
|
||||
|
||||
for (const [component, details] of Object.entries(summary)) {
|
||||
if (details.files === 0) throw new Error(`Release component is empty: ${component}`);
|
||||
}
|
||||
await fs.writeFile(path.join(output, "components.json"), `${JSON.stringify(summary, null, 2)}\n`, { flag: "wx", mode: 0o644 });
|
||||
process.stdout.write(`${JSON.stringify(summary)}\n`);
|
||||
|
||||
function normalizeRelative(value) {
|
||||
const normalized = path.posix.normalize(value.replaceAll("\\", "/"));
|
||||
if (!normalized || normalized === "." || normalized === ".." || normalized.startsWith("../") || path.posix.isAbsolute(normalized)) {
|
||||
throw new Error(`Unsafe manifest path: ${value}`);
|
||||
}
|
||||
return normalized;
|
||||
}
|
||||
|
||||
function safeJoin(root, relative) {
|
||||
const target = path.resolve(root, ...relative.split("/"));
|
||||
const relation = path.relative(root, target);
|
||||
if (relation === ".." || relation.startsWith(`..${path.sep}`) || path.isAbsolute(relation)) {
|
||||
throw new Error(`Path escaped component root: ${relative}`);
|
||||
}
|
||||
return target;
|
||||
}
|
||||
|
||||
function sha256File(file) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const hash = createHash("sha256");
|
||||
const stream = createReadStream(file);
|
||||
stream.on("data", (chunk) => hash.update(chunk));
|
||||
stream.once("error", reject);
|
||||
stream.once("end", () => resolve(hash.digest("hex")));
|
||||
});
|
||||
}
|
||||
@@ -1,8 +1,12 @@
|
||||
name: Workflow Lint
|
||||
on: [push, pull_request]
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
actionlint:
|
||||
runs-on: ubicloud-standard-8
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: rhysd/actionlint@v1.7.11
|
||||
- uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
|
||||
- uses: rhysd/actionlint@393031adb9afb225ee52ae2ccd7a5af5525e03e8 # v1.7.11
|
||||
|
||||
@@ -1,4 +1,7 @@
|
||||
name: Build CI Image
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
on:
|
||||
# Rebuild weekly (Monday 6am UTC) to pick up CLI updates
|
||||
schedule:
|
||||
@@ -20,18 +23,18 @@ jobs:
|
||||
contents: read
|
||||
packages: write
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
|
||||
|
||||
# Copy lockfile + package.json into Docker build context
|
||||
- run: cp package.json bun.lock .github/docker/
|
||||
|
||||
- uses: docker/login-action@v3
|
||||
- uses: docker/login-action@c94ce9fb468520275223c153574b00df6fe4bcc9 # v3
|
||||
with:
|
||||
registry: ghcr.io
|
||||
username: ${{ github.actor }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- uses: docker/build-push-action@v6
|
||||
- uses: docker/build-push-action@10e90e3645eae34f1e60eeb005ba3a3d33f178e8 # v6
|
||||
with:
|
||||
context: .github/docker
|
||||
file: .github/docker/Dockerfile.ci
|
||||
|
||||
@@ -1,4 +1,7 @@
|
||||
name: Periodic Evals
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
on:
|
||||
schedule:
|
||||
- cron: '0 6 * * 1' # Monday 6 AM UTC
|
||||
@@ -22,12 +25,12 @@ jobs:
|
||||
outputs:
|
||||
image-tag: ${{ steps.meta.outputs.tag }}
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
|
||||
|
||||
- id: meta
|
||||
run: echo "tag=${{ env.IMAGE }}:${{ hashFiles('.github/docker/Dockerfile.ci', 'package.json', 'bun.lock') }}" >> "$GITHUB_OUTPUT"
|
||||
|
||||
- uses: docker/login-action@v3
|
||||
- uses: docker/login-action@c94ce9fb468520275223c153574b00df6fe4bcc9 # v3
|
||||
with:
|
||||
registry: ghcr.io
|
||||
username: ${{ github.actor }}
|
||||
@@ -46,7 +49,7 @@ jobs:
|
||||
run: cp package.json bun.lock .github/docker/
|
||||
|
||||
- if: steps.check.outputs.exists == 'false'
|
||||
uses: docker/build-push-action@v6
|
||||
uses: docker/build-push-action@10e90e3645eae34f1e60eeb005ba3a3d33f178e8 # v6
|
||||
with:
|
||||
context: .github/docker
|
||||
file: .github/docker/Dockerfile.ci
|
||||
@@ -88,7 +91,7 @@ jobs:
|
||||
- name: e2e-gemini
|
||||
file: test/gemini-e2e.test.ts
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
@@ -126,7 +129,7 @@ jobs:
|
||||
|
||||
- name: Upload eval results
|
||||
if: always()
|
||||
uses: actions/upload-artifact@v4
|
||||
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4
|
||||
with:
|
||||
name: eval-periodic-${{ matrix.suite.name }}
|
||||
path: ~/.gstack-dev/evals/*.json
|
||||
|
||||
@@ -1,4 +1,7 @@
|
||||
name: E2E Evals
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
branches: [main]
|
||||
@@ -15,6 +18,8 @@ env:
|
||||
jobs:
|
||||
# Build Docker image with pre-baked toolchain (cached — only rebuilds on Dockerfile/lockfile change)
|
||||
build-image:
|
||||
# Paid secrets and package-write credentials never run on fork code.
|
||||
if: github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository
|
||||
runs-on: ubicloud-standard-8
|
||||
permissions:
|
||||
contents: read
|
||||
@@ -22,12 +27,12 @@ jobs:
|
||||
outputs:
|
||||
image-tag: ${{ steps.meta.outputs.tag }}
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
|
||||
|
||||
- id: meta
|
||||
run: echo "tag=${{ env.IMAGE }}:${{ hashFiles('.github/docker/Dockerfile.ci', 'package.json', 'bun.lock') }}" >> "$GITHUB_OUTPUT"
|
||||
|
||||
- uses: docker/login-action@v3
|
||||
- uses: docker/login-action@c94ce9fb468520275223c153574b00df6fe4bcc9 # v3
|
||||
with:
|
||||
registry: ghcr.io
|
||||
username: ${{ github.actor }}
|
||||
@@ -46,7 +51,7 @@ jobs:
|
||||
run: cp package.json bun.lock .github/docker/
|
||||
|
||||
- if: steps.check.outputs.exists == 'false'
|
||||
uses: docker/build-push-action@v6
|
||||
uses: docker/build-push-action@10e90e3645eae34f1e60eeb005ba3a3d33f178e8 # v6
|
||||
with:
|
||||
context: .github/docker
|
||||
file: .github/docker/Dockerfile.ci
|
||||
@@ -56,6 +61,7 @@ jobs:
|
||||
${{ env.IMAGE }}:latest
|
||||
|
||||
evals:
|
||||
if: github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository
|
||||
runs-on: ${{ matrix.suite.runner || 'ubicloud-standard-8' }}
|
||||
needs: build-image
|
||||
container:
|
||||
@@ -105,7 +111,7 @@ jobs:
|
||||
file: test/skill-e2e-office-hours-auto-mode.test.ts test/skill-e2e-plan-mode-no-op.test.ts
|
||||
timeout: 35
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
@@ -254,7 +260,7 @@ jobs:
|
||||
|
||||
- name: Upload eval results
|
||||
if: always()
|
||||
uses: actions/upload-artifact@v4
|
||||
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4
|
||||
with:
|
||||
name: eval-${{ matrix.suite.name }}
|
||||
path: ~/.gstack-dev/evals/*.json
|
||||
@@ -263,7 +269,7 @@ jobs:
|
||||
report:
|
||||
runs-on: ubicloud-standard-8
|
||||
needs: evals
|
||||
if: always() && github.event_name == 'pull_request'
|
||||
if: always() && github.event_name == 'pull_request' && github.event.pull_request.head.repo.full_name == github.repository
|
||||
timeout-minutes: 5
|
||||
permissions:
|
||||
contents: read
|
||||
@@ -275,12 +281,12 @@ jobs:
|
||||
# early and never hit it, which is why this stayed hidden). See #1802 CI fix.
|
||||
issues: write
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
|
||||
with:
|
||||
fetch-depth: 1
|
||||
|
||||
- name: Download all eval artifacts
|
||||
uses: actions/download-artifact@v4
|
||||
uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4
|
||||
with:
|
||||
pattern: eval-*
|
||||
path: /tmp/eval-results
|
||||
|
||||
@@ -30,7 +30,7 @@ jobs:
|
||||
bun-version: 1.3.14
|
||||
- uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4
|
||||
with:
|
||||
node-version: 22
|
||||
node-version: 22.23.1
|
||||
- name: Configure isolated test identity
|
||||
run: |
|
||||
git config --global user.email "gstack2-ci@example.invalid"
|
||||
@@ -56,10 +56,27 @@ jobs:
|
||||
bun-version: 1.3.14
|
||||
- uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4
|
||||
with:
|
||||
node-version: 22
|
||||
node-version: 22.23.1
|
||||
- run: bun install --frozen-lockfile
|
||||
- run: bun run test:gstack2:install
|
||||
|
||||
installed-first-use:
|
||||
name: Installed six-skill first use
|
||||
runs-on: ubuntu-24.04
|
||||
timeout-minutes: 15
|
||||
steps:
|
||||
- uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
|
||||
- uses: oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6 # v2
|
||||
with:
|
||||
bun-version: 1.3.14
|
||||
- uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4
|
||||
with:
|
||||
node-version: 22.23.1
|
||||
- run: bun install --frozen-lockfile --ignore-scripts
|
||||
- run: bun run gen:gstack2
|
||||
- name: Prove consent-first capability setup continues after approval
|
||||
run: bun test --timeout 30000 test/gstack2-installation.test.ts test/gstack2-skill-ux.test.ts
|
||||
|
||||
dev-container:
|
||||
runs-on: ubuntu-24.04
|
||||
timeout-minutes: 25
|
||||
|
||||
@@ -1,4 +1,7 @@
|
||||
name: make-pdf copy-paste gate
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
branches: [main]
|
||||
@@ -36,11 +39,11 @@ jobs:
|
||||
|
||||
runs-on: ${{ matrix.os }}
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
|
||||
|
||||
- uses: oven-sh/setup-bun@v2
|
||||
- uses: oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6 # v2
|
||||
with:
|
||||
bun-version: latest
|
||||
bun-version: 1.3.14
|
||||
|
||||
- name: Install dependencies
|
||||
run: bun install --frozen-lockfile
|
||||
|
||||
@@ -1,5 +1,8 @@
|
||||
name: PR Title Sync
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
# WHY pull_request_target (not pull_request): the default GITHUB_TOKEN is
|
||||
# READ-ONLY on fork PRs under `pull_request`, so the title-sync backstop could
|
||||
# never `gh pr edit` a fork/agent PR. `pull_request_target` runs in the base-repo
|
||||
@@ -39,7 +42,7 @@ jobs:
|
||||
steps:
|
||||
# Base repo only — trusted infra (the rewrite helper). No PR-head checkout.
|
||||
- name: Checkout base repo (trusted)
|
||||
uses: actions/checkout@v4
|
||||
uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
|
||||
with:
|
||||
fetch-depth: 1
|
||||
|
||||
|
||||
@@ -0,0 +1,75 @@
|
||||
name: Release quality gate
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
branches: [main]
|
||||
push:
|
||||
branches: [main]
|
||||
workflow_dispatch:
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
quality:
|
||||
runs-on: ubuntu-24.04
|
||||
timeout-minutes: 20
|
||||
steps:
|
||||
- uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
- uses: oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6 # v2
|
||||
with:
|
||||
bun-version: 1.3.14
|
||||
- uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4
|
||||
with:
|
||||
node-version: 22.23.1
|
||||
|
||||
- name: Install frozen dependencies
|
||||
run: bun install --frozen-lockfile --ignore-scripts
|
||||
|
||||
- name: Release and package contract tests
|
||||
run: bun run check:release
|
||||
|
||||
- name: Scan changed production text for credentials
|
||||
env:
|
||||
BASE_SHA: ${{ github.event.pull_request.base.sha || github.event.before }}
|
||||
HEAD_SHA: ${{ github.event.pull_request.head.sha || github.sha }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
if ! git cat-file -e "${BASE_SHA}^{commit}" 2>/dev/null; then
|
||||
BASE_SHA=$(git rev-parse HEAD^)
|
||||
fi
|
||||
git diff --unified=0 --no-color "$BASE_SHA" "$HEAD_SHA" -- \
|
||||
. \
|
||||
':(exclude)test/**' \
|
||||
':(exclude)evals/**' \
|
||||
':(exclude)outputs/**' \
|
||||
':(exclude)lib/diagram-render/dist/**' \
|
||||
':(exclude)skills/*/references/support/**' \
|
||||
':(exclude)docs/gstack-2/BACKLOG-MAP.json' \
|
||||
| node .github/scripts/gate-secret-scan.mjs
|
||||
|
||||
- name: Pack, install, and invoke the npm runtime-control package
|
||||
run: node .github/scripts/smoke-packed-package.mjs "$GITHUB_WORKSPACE"
|
||||
|
||||
- name: Gate critical dependency advisories
|
||||
run: bun audit --audit-level=critical
|
||||
|
||||
- name: Syntax-check runtime JavaScript
|
||||
run: find runtime -type f \( -name '*.js' -o -name '*.mjs' \) -print0 | xargs -0 -n1 node --check
|
||||
|
||||
- name: Install ShellCheck
|
||||
run: |
|
||||
sudo apt-get update
|
||||
sudo apt-get install -y shellcheck=0.9.0-1
|
||||
shellcheck --version
|
||||
|
||||
- name: ShellCheck release and setup boundaries
|
||||
run: >-
|
||||
shellcheck --severity=error
|
||||
setup
|
||||
scripts/build.sh
|
||||
scripts/write-version-files.sh
|
||||
scripts/gstack2/runtime-install-smoke.sh
|
||||
browse/scripts/build-node-server.sh
|
||||
@@ -0,0 +1,198 @@
|
||||
name: Release runtime artifacts
|
||||
|
||||
on:
|
||||
push:
|
||||
tags: [v2.0.0]
|
||||
workflow_dispatch:
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
concurrency:
|
||||
group: runtime-release-${{ github.ref }}
|
||||
cancel-in-progress: false
|
||||
|
||||
jobs:
|
||||
build:
|
||||
name: Build ${{ matrix.target }}
|
||||
permissions:
|
||||
contents: read
|
||||
id-token: write
|
||||
attestations: write
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
include:
|
||||
- os: macos-15
|
||||
target: darwin-arm64
|
||||
capabilities: browser,browser-visible,design,pdf,diagram,ios
|
||||
- os: macos-15-intel
|
||||
target: darwin-x64
|
||||
capabilities: browser,browser-visible,design,pdf,diagram,ios
|
||||
- os: ubuntu-24.04-arm
|
||||
target: linux-arm64
|
||||
capabilities: browser,browser-visible,design,pdf,diagram
|
||||
- os: ubuntu-24.04
|
||||
target: linux-x64
|
||||
capabilities: browser,browser-visible,design,pdf,diagram
|
||||
- os: windows-11-arm
|
||||
target: windows-arm64
|
||||
capabilities: browser,browser-visible,design,pdf,diagram
|
||||
- os: windows-2025
|
||||
target: windows-x64
|
||||
capabilities: browser,browser-visible,design,pdf,diagram
|
||||
runs-on: ${{ matrix.os }}
|
||||
timeout-minutes: 35
|
||||
steps:
|
||||
- uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
|
||||
- uses: oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6 # v2
|
||||
with:
|
||||
bun-version: 1.3.14
|
||||
- uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4
|
||||
with:
|
||||
node-version: 22.23.1
|
||||
- uses: sigstore/cosign-installer@d7543c93d881b35a8faa02e8e3605f69b7a1ce62 # v3.10.0
|
||||
|
||||
- name: Install frozen dependencies
|
||||
run: bun install --frozen-lockfile --ignore-scripts
|
||||
shell: bash
|
||||
|
||||
- name: Build and stage the complete managed runtime
|
||||
env:
|
||||
GSTACK_HOME: ${{ runner.temp }}/gstack-release-home
|
||||
TARGET: ${{ matrix.target }}
|
||||
CAPABILITIES: ${{ matrix.capabilities }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
node runtime/install.js \
|
||||
--source "$GITHUB_WORKSPACE" \
|
||||
--home "$GSTACK_HOME" \
|
||||
--version 2.0.0 \
|
||||
--install-now \
|
||||
--yes \
|
||||
--capabilities "$CAPABILITIES"
|
||||
active_slot=$(node -e 'const fs=require("fs"),p=process.argv[1];const v=JSON.parse(fs.readFileSync(p,"utf8")).current;if(typeof v!=="string"||!/^[A-Za-z0-9][A-Za-z0-9._-]{0,127}$/.test(v))process.exit(1);process.stdout.write(v)' "$GSTACK_HOME/versions/current.json")
|
||||
active="$GSTACK_HOME/versions/$active_slot"
|
||||
test -f "$active/.gstack-bundle.json"
|
||||
case "$TARGET" in
|
||||
windows-*) managed_bun_rel=".gstack-runtime-tools/bun.exe" ;;
|
||||
*) managed_bun_rel=".gstack-runtime-tools/bun" ;;
|
||||
esac
|
||||
managed_bun="$active/$managed_bun_rel"
|
||||
test -f "$managed_bun"
|
||||
test "$("$managed_bun" --version)" = "1.3.14"
|
||||
test -f "$active/runtime/licenses/BUN-LICENSE-1.3.14.md"
|
||||
test -f "$active/runtime/licenses/BUN-SOURCE.md"
|
||||
node -e 'const fs=require("fs"),c=require("crypto"),root=process.argv[1],rel=process.argv[2];const m=JSON.parse(fs.readFileSync(root+"/.gstack-bundle.json","utf8"));if(m.tools?.bun?.path!==rel||m.tools?.bun?.version!=="1.3.14")process.exit(1);const license=fs.readFileSync(root+"/runtime/licenses/BUN-LICENSE-1.3.14.md");if(c.createHash("sha256").update(license).digest("hex")!=="2cb858b2db8fc793bca2093489c5bc8eee615d002cc4924254904044c27a0afa")process.exit(1)' "$active" "$managed_bun_rel"
|
||||
test -d "$active/.gstack-runtime-browsers"
|
||||
(
|
||||
cd "$active"
|
||||
PLAYWRIGHT_BROWSERS_PATH="$active/.gstack-runtime-browsers" \
|
||||
node --input-type=module --eval \
|
||||
'const { chromium } = await import("./node_modules/playwright/index.mjs"); for (const options of [{ headless: true }, { headless: true, channel: "chromium" }]) { const browser = await chromium.launch(options); await browser.close(); }'
|
||||
)
|
||||
node_command=$(node -p 'process.execPath')
|
||||
host_bun=$(command -v bun)
|
||||
host_bun_dir=$(cd "$(dirname "$host_bun")" && pwd -P)
|
||||
clean_path=""
|
||||
IFS=: read -r -a path_parts <<< "$PATH"
|
||||
for part in "${path_parts[@]}"; do
|
||||
physical=$(cd "$part" 2>/dev/null && pwd -P || printf '%s' "$part")
|
||||
if [ "$physical" != "$host_bun_dir" ]; then
|
||||
clean_path="${clean_path:+$clean_path:}$part"
|
||||
fi
|
||||
done
|
||||
if (PATH="$clean_path"; command -v bun >/dev/null 2>&1); then
|
||||
echo "Host-global Bun remained available after removing setup-bun from PATH" >&2
|
||||
exit 1
|
||||
fi
|
||||
test "$(PATH="$clean_path" GSTACK_NODE="$node_command" "$GSTACK_HOME/bin/bun" --version)" = "1.3.14"
|
||||
browser_cleanup() {
|
||||
PATH="$clean_path" GSTACK_NODE="$node_command" BROWSE_PARENT_PID=0 \
|
||||
"$GSTACK_HOME/bin/browse" stop >/dev/null 2>&1 || true
|
||||
}
|
||||
trap browser_cleanup EXIT
|
||||
PATH="$clean_path" GSTACK_NODE="$node_command" BROWSE_PARENT_PID=0 \
|
||||
"$GSTACK_HOME/bin/browse" goto about:blank
|
||||
PATH="$clean_path" GSTACK_NODE="$node_command" BROWSE_PARENT_PID=0 \
|
||||
"$GSTACK_HOME/bin/browse" status
|
||||
browser_cleanup
|
||||
trap - EXIT
|
||||
stage="$RUNNER_TEMP/runtime-components"
|
||||
mkdir -p "$stage" "$GITHUB_WORKSPACE/release-output"
|
||||
node .github/scripts/stage-runtime-components.mjs "$active" "$stage"
|
||||
for component_dir in "$stage"/*; do
|
||||
test -d "$component_dir" || continue
|
||||
component=$(basename "$component_dir")
|
||||
archive="$GITHUB_WORKSPACE/release-output/gstack-runtime-2.0.0-$TARGET-$component.tar.gz"
|
||||
tar -czf "$archive" -C "$component_dir" gstack
|
||||
node -e 'const fs=require("fs"),c=require("crypto"),p=process.argv[1];const b=fs.readFileSync(p);fs.writeFileSync(p+".sha256",c.createHash("sha256").update(b).digest("hex")+" "+require("path").basename(p)+"\n")' "$archive"
|
||||
done
|
||||
shell: bash
|
||||
|
||||
- name: Keyless-sign component archives
|
||||
run: |
|
||||
set -euo pipefail
|
||||
for archive in release-output/*.tar.gz; do
|
||||
cosign sign-blob --yes --bundle "$archive.sigstore.json" "$archive"
|
||||
done
|
||||
shell: bash
|
||||
|
||||
- name: Attest component archive provenance
|
||||
uses: actions/attest-build-provenance@e8998f949152b193b063cb0ec769d69d929409be # v2
|
||||
with:
|
||||
subject-path: release-output/*.tar.gz
|
||||
|
||||
- name: Upload signed archive
|
||||
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4
|
||||
with:
|
||||
name: runtime-${{ matrix.target }}
|
||||
path: release-output/*
|
||||
if-no-files-found: error
|
||||
retention-days: 14
|
||||
|
||||
manifest:
|
||||
name: Assemble manifest and GitHub Release
|
||||
needs: build
|
||||
runs-on: ubuntu-24.04
|
||||
if: startsWith(github.ref, 'refs/tags/v')
|
||||
permissions:
|
||||
contents: write
|
||||
id-token: write
|
||||
attestations: write
|
||||
steps:
|
||||
- uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
|
||||
- uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4
|
||||
with:
|
||||
pattern: runtime-*
|
||||
path: release-output
|
||||
merge-multiple: true
|
||||
- uses: sigstore/cosign-installer@d7543c93d881b35a8faa02e8e3605f69b7a1ce62 # v3.10.0
|
||||
|
||||
- name: Create strict six-target manifest
|
||||
run: node .github/scripts/create-runtime-release-manifest.mjs release-output "$GITHUB_REPOSITORY" 2.0.0
|
||||
|
||||
- name: Checksum and keyless-sign manifest
|
||||
run: |
|
||||
set -euo pipefail
|
||||
cd release-output
|
||||
sha256sum gstack-runtime-manifest.json > gstack-runtime-manifest.json.sha256
|
||||
cosign sign-blob --yes --bundle gstack-runtime-manifest.json.sigstore.json gstack-runtime-manifest.json
|
||||
shell: bash
|
||||
|
||||
- name: Attest manifest provenance
|
||||
uses: actions/attest-build-provenance@e8998f949152b193b063cb0ec769d69d929409be # v2
|
||||
with:
|
||||
subject-path: release-output/gstack-runtime-manifest.json
|
||||
|
||||
- name: Publish immutable release assets
|
||||
env:
|
||||
GH_TOKEN: ${{ github.token }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
gh release create "$GITHUB_REF_NAME" \
|
||||
--verify-tag \
|
||||
--title "GStack runtime 2.0.0" \
|
||||
--notes "Signed optional runtime artifacts for the six portable GStack skills." \
|
||||
release-output/*
|
||||
shell: bash
|
||||
@@ -1,11 +1,15 @@
|
||||
name: Skill Docs Freshness
|
||||
on: [push, pull_request]
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
check-freshness:
|
||||
runs-on: ubicloud-standard-8
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: oven-sh/setup-bun@v2
|
||||
- uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
|
||||
- uses: oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6 # v2
|
||||
- run: bun install
|
||||
- name: Check Claude host freshness
|
||||
run: bun run gen:skill-docs
|
||||
|
||||
@@ -1,5 +1,8 @@
|
||||
name: Version Gate
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
paths:
|
||||
@@ -20,13 +23,13 @@ jobs:
|
||||
pull-requests: read
|
||||
steps:
|
||||
- name: Checkout PR head
|
||||
uses: actions/checkout@v4
|
||||
uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
ref: ${{ github.event.pull_request.head.sha }}
|
||||
|
||||
- name: Setup Bun
|
||||
uses: oven-sh/setup-bun@v2
|
||||
uses: oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6 # v2
|
||||
|
||||
- name: Read versions
|
||||
id: versions
|
||||
|
||||
@@ -1,5 +1,8 @@
|
||||
name: Windows Free Tests
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
# Curated subset of the free test suite that runs on a paid faster Windows runner.
|
||||
#
|
||||
# Codex's v1.18.0.0 review flagged that the existing evals.yml workflow uses
|
||||
@@ -39,11 +42,11 @@ jobs:
|
||||
timeout-minutes: 15
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
|
||||
|
||||
- uses: oven-sh/setup-bun@v1
|
||||
- uses: oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6 # v2
|
||||
with:
|
||||
bun-version: latest
|
||||
bun-version: 1.3.14
|
||||
|
||||
- name: Configure git identity (required by tests that init temp repos)
|
||||
run: |
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
name: Windows Setup E2E
|
||||
|
||||
# End-to-end fresh-install gate for Windows. Runs `./setup` on a clean
|
||||
# windows-latest checkout and asserts the build completes, binaries
|
||||
# resolve via find-browse, and the gstack-paths state root resolves
|
||||
# cleanly. Catches Bun shell-parser regressions in package.json's build
|
||||
# chain (#1538, #1537, #1530, #1457, #1561) before they reach users.
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
# End-to-end optional-runtime gate for Windows. It first proves dry-run is
|
||||
# non-mutating, then performs an explicit browser-capability install, checks
|
||||
# doctor and the native artifact, and uninstalls. Agent Skills host placement
|
||||
# is deliberately outside this workflow and is covered by gstack2-gate.yml.
|
||||
#
|
||||
# Separate from windows-free-tests.yml because that one runs a curated
|
||||
# unit-test subset; this one exercises the install path itself.
|
||||
@@ -19,6 +21,7 @@ on:
|
||||
- 'scripts/build.sh'
|
||||
- 'scripts/write-version-files.sh'
|
||||
- 'setup'
|
||||
- 'runtime/**'
|
||||
- 'browse/src/cli.ts'
|
||||
- 'browse/src/find-browse.ts'
|
||||
- 'bin/gstack-paths'
|
||||
@@ -35,11 +38,18 @@ jobs:
|
||||
timeout-minutes: 15
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
|
||||
|
||||
- uses: oven-sh/setup-bun@v1
|
||||
- name: Configure isolated runtime home
|
||||
run: echo "GSTACK_HOME=$RUNNER_TEMP/gstack-setup-e2e" >> "$GITHUB_ENV"
|
||||
shell: bash
|
||||
|
||||
- uses: oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6 # v2
|
||||
with:
|
||||
bun-version: latest
|
||||
bun-version: 1.3.14
|
||||
- uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4
|
||||
with:
|
||||
node-version: 22.23.1
|
||||
|
||||
- name: Configure git identity
|
||||
run: |
|
||||
@@ -52,45 +62,34 @@ jobs:
|
||||
run: bun install --frozen-lockfile
|
||||
shell: bash
|
||||
|
||||
- name: Run bun run build (the previously-broken path)
|
||||
# This is the regression gate. Bun's Windows shell parser rejected
|
||||
# multiple constructs the old inline build chain used; the wave
|
||||
# moved the build to scripts/build.sh. If this step fails on
|
||||
# Windows, the build chain regressed.
|
||||
run: bun run build
|
||||
shell: bash
|
||||
env:
|
||||
GSTACK_SKIP_PLAYWRIGHT: '1'
|
||||
|
||||
- name: Verify binaries exist (with .exe extension on Windows)
|
||||
- name: Preview without mutating state
|
||||
run: |
|
||||
set -e
|
||||
test -f browse/dist/browse.exe || test -f browse/dist/browse || (echo "MISSING: browse" && exit 1)
|
||||
test -f browse/dist/find-browse.exe || test -f browse/dist/find-browse || (echo "MISSING: find-browse" && exit 1)
|
||||
test -f design/dist/design.exe || test -f design/dist/design || (echo "MISSING: design" && exit 1)
|
||||
test -f bin/gstack-global-discover.exe || test -f bin/gstack-global-discover || (echo "MISSING: gstack-global-discover" && exit 1)
|
||||
echo "All binaries present"
|
||||
bash ./setup --dry-run --capabilities browser
|
||||
test ! -e "$GSTACK_HOME" || (echo "dry-run mutated GSTACK_HOME" && exit 1)
|
||||
shell: bash
|
||||
|
||||
- name: Verify find-browse resolves to the .exe variant
|
||||
- name: Explicitly install the browser capability
|
||||
run: |
|
||||
set -e
|
||||
OUT=$(bun browse/src/find-browse.ts 2>&1) || true
|
||||
echo "find-browse output: $OUT"
|
||||
# On Windows, find-browse should successfully resolve to a binary,
|
||||
# whether or not it has the .exe extension on disk. Empty output
|
||||
# or "not found" means the .exe extension resolver regressed.
|
||||
echo "$OUT" | grep -qE '(browse\.exe|browse)$' || (echo "find-browse failed to resolve binary on Windows" && exit 1)
|
||||
bash ./setup --install-now --yes --capabilities browser
|
||||
test -f "$GSTACK_HOME/versions/current.json"
|
||||
test -f "$GSTACK_HOME/bin/gstack.cmd"
|
||||
shell: bash
|
||||
|
||||
- name: Verify gstack-paths state root resolves
|
||||
- name: Verify doctor and installed native browser
|
||||
run: |
|
||||
set -e
|
||||
eval "$(bash bin/gstack-paths)"
|
||||
test -n "$GSTACK_STATE_ROOT" || (echo "GSTACK_STATE_ROOT empty" && exit 1)
|
||||
test -n "$PLAN_ROOT" || (echo "PLAN_ROOT empty" && exit 1)
|
||||
test -n "$TMP_ROOT" || (echo "TMP_ROOT empty" && exit 1)
|
||||
echo "GSTACK_STATE_ROOT=$GSTACK_STATE_ROOT"
|
||||
echo "PLAN_ROOT=$PLAN_ROOT"
|
||||
echo "TMP_ROOT=$TMP_ROOT"
|
||||
node runtime/cli.js doctor --json > doctor.json
|
||||
node -e 'const r=require("./doctor.json");if(!r.checks.some(x=>x.id==="managed-runtime"&&x.status==="pass"))process.exit(1);if(!r.checks.some(x=>x.id==="capability:browser"&&x.status==="pass"))process.exit(1)'
|
||||
browser=$(node runtime/cli.js runtime path browse/dist/browse.exe)
|
||||
test -f "$browser"
|
||||
shell: bash
|
||||
|
||||
- name: Uninstall the runtime and preserve user state
|
||||
run: |
|
||||
set -e
|
||||
node runtime/cli.js uninstall
|
||||
test ! -e "$GSTACK_HOME/versions/current.json"
|
||||
test -d "$GSTACK_HOME/projects"
|
||||
shell: bash
|
||||
|
||||
Reference in New Issue
Block a user