Skip to content

Commit 9bbb3b9

Browse files
committed
ci: Harden release workflow handling
1 parent cbe31ef commit 9bbb3b9

6 files changed

Lines changed: 91 additions & 24 deletions

File tree

.github/actions/find-rust-changes/action.yml

Lines changed: 15 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -11,24 +11,29 @@ runs:
1111
- name: Check for Rust changes
1212
id: check
1313
shell: bash
14+
env:
15+
EVENT_NAME: ${{ github.event_name }}
16+
BASE_REF: ${{ github.base_ref }}
17+
BEFORE_SHA: ${{ github.event.before }}
18+
AFTER_SHA: ${{ github.event.after }}
1419
run: |
15-
if [ "${{ github.event_name }}" == "pull_request" ]; then
16-
git fetch origin ${{ github.base_ref }}
17-
BASE_COMMIT="origin/${{ github.base_ref }}"
20+
if [ "$EVENT_NAME" = "pull_request" ]; then
21+
git fetch origin "$BASE_REF"
22+
BASE_COMMIT="origin/${BASE_REF}"
1823
HEAD_COMMIT="HEAD"
1924
else
20-
BASE_COMMIT="${{ github.event.before }}"
21-
HEAD_COMMIT="${{ github.event.after }}"
25+
BASE_COMMIT="$BEFORE_SHA"
26+
HEAD_COMMIT="$AFTER_SHA"
2227
fi
2328
24-
echo "Comparing changes between $BASE_COMMIT and $HEAD_COMMIT"
29+
echo "Comparing changes between ${BASE_COMMIT} and ${HEAD_COMMIT}"
2530
26-
RUST_PATHS="crates/ Cargo.toml Cargo.lock .cargo/ rust-toolchain.toml"
31+
RUST_PATHS=("crates/" "Cargo.toml" "Cargo.lock" ".cargo/" "rust-toolchain.toml")
2732
28-
if git diff --name-only $BASE_COMMIT $HEAD_COMMIT -- $RUST_PATHS | grep -q .; then
29-
echo "changed=true" >> $GITHUB_OUTPUT
33+
if git diff --name-only "$BASE_COMMIT" "$HEAD_COMMIT" -- "${RUST_PATHS[@]}" | grep -q .; then
34+
echo "changed=true" >> "$GITHUB_OUTPUT"
3035
echo "Changes detected in Rust paths"
3136
else
32-
echo "changed=false" >> $GITHUB_OUTPUT
37+
echo "changed=false" >> "$GITHUB_OUTPUT"
3338
echo "No changes in Rust paths"
3439
fi

.github/workflows/docs-alias-failure-notification.yml

Lines changed: 14 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,9 @@ on:
1010
types:
1111
- completed
1212

13+
permissions:
14+
contents: read
15+
1316
jobs:
1417
notify-failure:
1518
name: "Notify Slack on Docs Alias Failure"
@@ -23,12 +26,19 @@ jobs:
2326

2427
- name: Get version
2528
id: version
29+
shell: bash
2630
run: |
2731
if [ -f version.txt ]; then
28-
VERSION=$(head -n 1 version.txt)
29-
echo "version=${VERSION}" >> $GITHUB_OUTPUT
32+
IFS= read -r VERSION < version.txt
33+
34+
if [[ "$VERSION" =~ ^[0-9]+\.[0-9]+\.[0-9]+(-[0-9A-Za-z.]+)?$ ]]; then
35+
printf 'version=%s\n' "$VERSION" >> "$GITHUB_OUTPUT"
36+
else
37+
echo "::warning::version.txt did not contain a valid release version. Reporting unknown version."
38+
echo "version=unknown" >> "$GITHUB_OUTPUT"
39+
fi
3040
else
31-
echo "version=unknown" >> $GITHUB_OUTPUT
41+
echo "version=unknown" >> "$GITHUB_OUTPUT"
3242
fi
3343
3444
- name: Send failure notification to Slack
@@ -38,5 +48,5 @@ jobs:
3848
webhook-type: incoming-webhook
3949
payload: |
4050
{
41-
"version": "${{ steps.version.outputs.version }}"
51+
"version": ${{ toJSON(steps.version.outputs.version) }}
4252
}

.github/workflows/turborepo-release.yml

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -538,10 +538,16 @@ jobs:
538538
- name: Find Vercel deployment for SHA
539539
id: find-deployment
540540
env:
541+
BASE_SHA: ${{ needs.stage.outputs.base-sha }}
541542
VERCEL_TOKEN: ${{ secrets.TURBO_TOKEN }}
542543
run: |
543-
SHA="${{ needs.stage.outputs.base-sha }}"
544-
DEPLOYMENT_URL=$(vercel list turbo-site --scope=vercel -m githubCommitSha="${SHA}" --status=READY --token="${VERCEL_TOKEN}" 2>&1 | tee /dev/stderr | grep -E '^\S+\.vercel\.(app|sh)' | head -n 1 | awk '{print $1}')
544+
SHA="${BASE_SHA}"
545+
VERCEL_LIST_STDERR="${RUNNER_TEMP}/vercel-list.stderr"
546+
if ! VERCEL_LIST_OUTPUT=$(vercel list turbo-site --scope=vercel -m githubCommitSha="${SHA}" --status=READY --token="${VERCEL_TOKEN}" 2>"${VERCEL_LIST_STDERR}"); then
547+
echo "::error::Failed to list Vercel deployments for SHA ${SHA}. Vercel CLI stderr was captured without printing to logs."
548+
exit 1
549+
fi
550+
DEPLOYMENT_URL=$(printf '%s\n' "${VERCEL_LIST_OUTPUT}" | grep -E '^\S+\.vercel\.(app|sh)' | head -n 1 | awk '{print $1}' || true)
545551
546552
if [ -z "$DEPLOYMENT_URL" ]; then
547553
echo "::error::No deployment found for SHA ${SHA}."

packages/turbo-releaser/src/native.test.ts

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import native from "./native";
66
import type { Platform } from "./types";
77

88
describe("generateNativePackage", () => {
9+
const outputBaseDir = "/path/to";
910
const outputDir = "/path/to/output";
1011

1112
it("should generate package correctly for non-Windows platform", async (t) => {
@@ -29,6 +30,7 @@ describe("generateNativePackage", () => {
2930
platform,
3031
version,
3132
outputDir,
33+
outputBaseDir,
3234
packagePrefix: "@turbo"
3335
});
3436

@@ -103,6 +105,7 @@ describe("generateNativePackage", () => {
103105
platform: { os: "windows", arch: "x64" },
104106
version: "1.0.0",
105107
outputDir,
108+
outputBaseDir,
106109
packagePrefix: "@turbo"
107110
});
108111

@@ -132,11 +135,28 @@ describe("generateNativePackage", () => {
132135
platform: { os: "linux", arch: "x64" },
133136
version: "1.2.0",
134137
outputDir,
138+
outputBaseDir,
135139
packagePrefix: "@turbo"
136140
}),
137141
{ message: "Failed to remove directory" }
138142
);
139143
});
144+
145+
it("should reject output directories outside the package base", async () => {
146+
await assert.rejects(
147+
native.generateNativePackage({
148+
platform: { os: "linux", arch: "x64" },
149+
version: "1.2.0",
150+
outputDir: "/path/elsewhere",
151+
outputBaseDir,
152+
packagePrefix: "@turbo"
153+
}),
154+
{
155+
message:
156+
"Refusing to clean output directory outside package base: /path/elsewhere"
157+
}
158+
);
159+
});
140160
});
141161

142162
describe("archToHuman", () => {

packages/turbo-releaser/src/native.ts

Lines changed: 30 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -25,27 +25,30 @@ async function generateNativePackage({
2525
platform,
2626
version,
2727
outputDir,
28+
outputBaseDir,
2829
packagePrefix = "turbo",
2930
description
3031
}: {
3132
platform: Platform;
3233
version: string;
3334
outputDir: string;
35+
outputBaseDir: string;
3436
packagePrefix?: string;
3537
description?: string;
3638
}) {
3739
const { os, arch } = platform;
40+
const safeOutputDir = resolveOutputDir(outputDir, outputBaseDir);
3841
console.log(`Generating native package for ${os}-${arch}...`);
3942

40-
console.log(`Cleaning output directory: ${outputDir}`);
41-
await rm(outputDir, { recursive: true, force: true });
42-
await mkdir(path.join(outputDir, "bin"), { recursive: true });
43+
console.log(`Cleaning output directory: ${safeOutputDir}`);
44+
await rm(safeOutputDir, { recursive: true, force: true });
45+
await mkdir(path.join(safeOutputDir, "bin"), { recursive: true });
4346

4447
const copyFromTemplate = async (part: string, ...parts: Array<string>) => {
4548
console.log("Copying ", path.join(part, ...parts));
4649
await copyFile(
4750
path.join(templateDir, part, ...parts),
48-
path.join(outputDir, part, ...parts)
51+
path.join(safeOutputDir, part, ...parts)
4952
);
5053
};
5154

@@ -77,11 +80,32 @@ async function generateNativePackage({
7780
packageJson.publishConfig = { access: "public" };
7881
}
7982
await writeFile(
80-
path.join(outputDir, "package.json"),
83+
path.join(safeOutputDir, "package.json"),
8184
JSON.stringify(packageJson, null, 2)
8285
);
8386

84-
console.log(`Native package generated successfully in ${outputDir}`);
87+
console.log(`Native package generated successfully in ${safeOutputDir}`);
88+
}
89+
90+
function resolveOutputDir(outputDir: string, outputBaseDir: string) {
91+
const resolvedOutputDir = path.resolve(outputDir);
92+
const resolvedOutputBaseDir = path.resolve(outputBaseDir);
93+
const relativeOutputDir = path.relative(
94+
resolvedOutputBaseDir,
95+
resolvedOutputDir
96+
);
97+
98+
if (
99+
relativeOutputDir === "" ||
100+
relativeOutputDir.startsWith("..") ||
101+
path.isAbsolute(relativeOutputDir)
102+
) {
103+
throw new Error(
104+
`Refusing to clean output directory outside package base: ${outputDir}`
105+
);
106+
}
107+
108+
return resolvedOutputDir;
85109
}
86110

87111
// Exported asn an object instead of export keyword, so that these functions

packages/turbo-releaser/src/operations.ts

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -69,14 +69,16 @@ async function packPlatform({
6969
.replace("@", "")
7070
.replace("/", "-");
7171
validatePathSegment("package directory name", npmDirName);
72-
const tarballDir = path.join(srcDir, "dist", `${npmDirName}-${version}`);
72+
const distDir = path.join(srcDir, "dist");
73+
const tarballDir = path.join(distDir, `${npmDirName}-${version}`);
7374
const scaffoldDir = path.join(tarballDir, npmDirName);
7475

7576
console.log("Generating native package...");
7677
await native.generateNativePackage({
7778
platform,
7879
version,
7980
outputDir: scaffoldDir,
81+
outputBaseDir: tarballDir,
8082
packagePrefix,
8183
description
8284
});
@@ -99,7 +101,7 @@ async function packPlatform({
99101

100102
console.log("Creating tar.gz...");
101103
const tarName = `${npmDirName}-${version}.tar.gz`;
102-
const tarPath = path.join(srcDir, "dist", tarName);
104+
const tarPath = path.join(distDir, tarName);
103105
await tar.create(
104106
{
105107
gzip: true,

0 commit comments

Comments
 (0)