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
@@ -1,2 +1,3 @@
* @BitGo/developer-experience
api.yaml @BitGo/developer-experience @BitGo/technical-writers
express-api.yaml @BitGo/developer-experience @BitGo/technical-writers
65 changes: 55 additions & 10 deletions .github/workflows/generate-release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ on:
- master
paths:
- 'api.yaml'
- 'express-api.yaml'

jobs:
generate-release:
Expand All @@ -24,17 +25,61 @@ jobs:
with:
node-version-file: .nvmrc

- name: Get API specs and generate JSON files
- name: Get Platform API specs and generate JSON files
run: |
PREVIOUS_MERGE=$(git rev-list --merges master | head -n 2 | tail -n 1)
git show $PREVIOUS_MERGE:api.yaml > previous.yaml || echo "v0.0.0" > previous.yaml
yq -o=json previous.yaml > previous.json
yq -o=json api.yaml > current.json
yq -o=json previous.yaml > previous-platform.json

yq -o=json api.yaml > current-platform.json
rm previous.yaml

- name: Run API diff
run: node scripts/api-diff.js
- name: Get Express API specs and generate JSON files
run: |
PREVIOUS_MERGE=$(git rev-list --merges master | head -n 2 | tail -n 1)
git show $PREVIOUS_MERGE:express-api.yaml > previous-express.yaml || echo "v0.0.0" > previous-express.yaml
yq -o=json previous-express.yaml > previous-express.json

if [ -f express-api.yaml ]; then
yq -o=json express-api.yaml > current-express.json
else
echo "{}" > current-express.json
fi
rm -f previous-express.yaml

- name: Run Platform API diff
run: node scripts/api-diff.js previous-platform.json current-platform.json platform-release.md

- name: Run Express API diff
run: node scripts/api-diff.js previous-express.json current-express.json express-release.md

- name: Combine release descriptions
run: |
PLATFORM_HAS_CONTENT=false
EXPRESS_HAS_CONTENT=false

if [ -s platform-release.md ]; then
PLATFORM_HAS_CONTENT=true
fi
if [ -s express-release.md ]; then
EXPRESS_HAS_CONTENT=true
fi

# Clear output
> release-description.md

if [ "$PLATFORM_HAS_CONTENT" = true ] && [ "$EXPRESS_HAS_CONTENT" = true ]; then
# Both have content — nest under headers
echo "## Platform API" >> release-description.md
sed 's/^## /### /' platform-release.md >> release-description.md
echo "" >> release-description.md
echo "## Express API" >> release-description.md
sed 's/^## /### /' express-release.md >> release-description.md
elif [ "$PLATFORM_HAS_CONTENT" = true ]; then
cat platform-release.md >> release-description.md
elif [ "$EXPRESS_HAS_CONTENT" = true ]; then
cat express-release.md >> release-description.md
fi

- name: Determine version
id: version
Expand All @@ -43,11 +88,11 @@ jobs:
YEAR=$(date +%Y)
MONTH=$(date +%m)
DAY=$(date +%d)

# Get the latest tag for current year.month.day
CURRENT_VERSION=$(git describe --tags --abbrev=0 2>/dev/null || echo "v$YEAR.$MONTH.$DAY.0")
echo "Current version: $CURRENT_VERSION"

# Extract version number
if [[ $CURRENT_VERSION == v$YEAR.$MONTH.$DAY.* ]]; then
# If we already have a tag for today, increment its number
Expand All @@ -57,7 +102,7 @@ jobs:
# If this is the first tag for today, start at .1
NEW_VERSION="v$YEAR.$MONTH.$DAY.1"
fi

echo "New version: $NEW_VERSION"
echo "new_version=$NEW_VERSION" >> $GITHUB_OUTPUT

Expand All @@ -76,7 +121,7 @@ jobs:
echo "Release summary is empty, skipping release creation"
exit 0
fi

# Create GitHub Release
gh release create ${{ steps.version.outputs.new_version }} \
--title "${{ steps.version.outputs.new_version }}" \
Expand Down
12 changes: 8 additions & 4 deletions scripts/api-diff.js
Original file line number Diff line number Diff line change
@@ -1,8 +1,12 @@
const fs = require('fs');

// Read the JSON files
const previousSpec = JSON.parse(fs.readFileSync('previous.json', 'utf8'));
const currentSpec = JSON.parse(fs.readFileSync('current.json', 'utf8'));
// Read the JSON files (paths configurable via CLI args)
const previousPath = process.argv[2] || 'previous.json';
const currentPath = process.argv[3] || 'current.json';
const outputPath = process.argv[4] || 'release-description.md';

const previousSpec = JSON.parse(fs.readFileSync(previousPath, 'utf8'));
const currentSpec = JSON.parse(fs.readFileSync(currentPath, 'utf8'));

// Initialize change tracking
const changes = {
Expand Down Expand Up @@ -491,4 +495,4 @@ detectRenamedEndpoints();
const releaseDescription = generateReleaseNotes();

// Write release notes to markdown file
fs.writeFileSync('release-description.md', releaseDescription);
fs.writeFileSync(outputPath, releaseDescription);
27 changes: 23 additions & 4 deletions tests/generate-release.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,9 +11,11 @@

beforeEach(() => {
// Clean up any existing output files
if (fs.existsSync('release-description.md')) {
fs.unlinkSync('release-description.md');
}
['release-description.md', 'custom-output.md', 'previous.json', 'current.json'].forEach(file => {
if (fs.existsSync(file)) {
fs.unlinkSync(file);
}
});
});

scenarios.forEach(scenario => {
Expand Down Expand Up @@ -43,9 +45,26 @@
});
});

it('supports custom file paths via CLI args', () => {
const scenarioDir = path.join(FIXTURES_DIR, 'new-route-and-method');

// Run the script with custom input/output paths
execSync(
`node scripts/api-diff.js ${path.join(scenarioDir, 'previous.json')} ${path.join(scenarioDir, 'current.json')} custom-output.md`

Check warning

Code scanning / CodeQL

Shell command built from environment values Medium test

This shell command depends on an uncontrolled
absolute path
.

Copilot Autofix

AI 3 months ago

In general, the best way to fix this kind of issue is to avoid building a single shell command string that is interpreted by a shell. Instead, call the program directly and pass arguments as an array to execFileSync/spawn or use the argument-array form of execSync, so the Node runtime handles argument escaping and no shell interpretation occurs.

For this specific test, we should change the vulnerable execSync call on line 53 to use the options form: execSync(command, options) with shell: false and args represented explicitly. Since we already import execSync from child_process, and execSync supports an array via its args in child_process.execFileSync but not in execSync, the more direct and clearer fix here is to switch this one call to execFileSync, which takes (file, args[, options]) and does not invoke a shell. That keeps behavior the same (running node scripts/api-diff.js with three CLI arguments) while safely handling any special characters in paths.

Concretely:

  • Add execFileSync to the destructured import from child_process at the top of tests/generate-release.test.js.
  • Replace the interpolated-string execSync call in the "supports custom file paths via CLI args" test with an execFileSync call: execFileSync('node', ['scripts/api-diff.js', path.join(...), path.join(...), 'custom-output.md']);.
  • Leave the earlier execSync('node scripts/api-diff.js'); alone, since it uses only hard-coded literals and is not part of the tainted flow.

No new helper methods are needed; just the additional import and the changed call.

Suggested changeset 1
tests/generate-release.test.js

Autofix patch

Autofix patch
Run the following command in your local git repository to apply this patch
cat << 'EOF' | git apply
diff --git a/tests/generate-release.test.js b/tests/generate-release.test.js
--- a/tests/generate-release.test.js
+++ b/tests/generate-release.test.js
@@ -1,6 +1,6 @@
 const fs = require('fs');
 const path = require('path');
-const { execSync } = require('child_process');
+const { execSync, execFileSync } = require('child_process');
 const { describe, it, beforeEach, afterEach } = require('node:test');
 const assert = require('node:assert');
 
@@ -49,9 +49,12 @@
     const scenarioDir = path.join(FIXTURES_DIR, 'new-route-and-method');
 
     // Run the script with custom input/output paths
-    execSync(
-      `node scripts/api-diff.js ${path.join(scenarioDir, 'previous.json')} ${path.join(scenarioDir, 'current.json')} custom-output.md`
-    );
+    execFileSync('node', [
+      'scripts/api-diff.js',
+      path.join(scenarioDir, 'previous.json'),
+      path.join(scenarioDir, 'current.json'),
+      'custom-output.md',
+    ]);
 
     const actual = fs.readFileSync('custom-output.md', 'utf8').trim();
     const expected = fs.readFileSync(
EOF
@@ -1,6 +1,6 @@
const fs = require('fs');
const path = require('path');
const { execSync } = require('child_process');
const { execSync, execFileSync } = require('child_process');
const { describe, it, beforeEach, afterEach } = require('node:test');
const assert = require('node:assert');

@@ -49,9 +49,12 @@
const scenarioDir = path.join(FIXTURES_DIR, 'new-route-and-method');

// Run the script with custom input/output paths
execSync(
`node scripts/api-diff.js ${path.join(scenarioDir, 'previous.json')} ${path.join(scenarioDir, 'current.json')} custom-output.md`
);
execFileSync('node', [
'scripts/api-diff.js',
path.join(scenarioDir, 'previous.json'),
path.join(scenarioDir, 'current.json'),
'custom-output.md',
]);

const actual = fs.readFileSync('custom-output.md', 'utf8').trim();
const expected = fs.readFileSync(
Copilot is powered by AI and may make mistakes. Always verify output.
);

const actual = fs.readFileSync('custom-output.md', 'utf8').trim();
const expected = fs.readFileSync(
path.join(scenarioDir, 'expected.md'),
'utf8'
).trim();

assert.strictEqual(actual, expected);
});

afterEach(() => {
// Clean up test files
['previous.json', 'current.json', 'release-description.md'].forEach(file => {
['previous.json', 'current.json', 'release-description.md', 'custom-output.md'].forEach(file => {
if (fs.existsSync(file)) {
fs.unlinkSync(file);
}
Expand Down