diff --git a/.github/actions/setup-ci/action.yml b/.github/actions/setup-ci/action.yml index e4a62a6..15a354f 100644 --- a/.github/actions/setup-ci/action.yml +++ b/.github/actions/setup-ci/action.yml @@ -17,8 +17,6 @@ runs: uses: actions/setup-node@v6 with: node-version: 24 - registry-url: https://npm.pkg.github.com - scope: software-hardware-integration-lab # Set up the socket firewall binary - name: Install - Socket Firewall diff --git a/.github/workflows/Build.yml b/.github/workflows/Build.yml index 85c17d9..9c2b22e 100644 --- a/.github/workflows/Build.yml +++ b/.github/workflows/Build.yml @@ -97,9 +97,9 @@ jobs: # Validate package integrity before publishing (tests, coverage, and production build). - name: Validate Package Before Publish - run: npm run validate:package + run: npm run validate:package:skip-reachability - # Generate the package archive that will be attested and published to each registry. + # Generate the package archive that will be attested and published to both registries. - name: Generate NPM Package Archive id: generate-package run: echo "package-file=$(npm pack --ignore-scripts)" >> "$GITHUB_OUTPUT" @@ -110,11 +110,13 @@ jobs: with: subject-path: ${{ steps.generate-package.outputs.package-file }} - # Publish the attested package archive to GitHub Packages for internal distribution. + # Publish the attested package to GitHub Packages without changing the npmjs install registry. - name: Upload Package to GitHub Packages Registry env: NODE_AUTH_TOKEN: ${{ secrets.GITHUB_TOKEN }} - run: npm publish ${{ steps.generate-package.outputs.package-file }} --tag ${{ needs.Metadata.outputs.channel }} --ignore-scripts + run: | + npm config set //npm.pkg.github.com/:_authToken "$NODE_AUTH_TOKEN" + npm publish ${{ steps.generate-package.outputs.package-file }} --registry=https://npm.pkg.github.com --tag ${{ needs.Metadata.outputs.channel }} --ignore-scripts # Upload the attested npm package archive for the publish workflow to consume. - name: Upload NPM Package Archive Artifact diff --git a/.github/workflows/Publish.yml b/.github/workflows/Publish.yml index 79270e1..6640695 100644 --- a/.github/workflows/Publish.yml +++ b/.github/workflows/Publish.yml @@ -51,7 +51,6 @@ jobs: background: true with: node-version: 24 - scope: software-hardware-integration-lab # Download the compiled server binary - name: Download Artifact From Build Job diff --git a/.github/workflows/Security-Reachability.yml b/.github/workflows/Security-Reachability.yml index f0978e6..76fa353 100644 --- a/.github/workflows/Security-Reachability.yml +++ b/.github/workflows/Security-Reachability.yml @@ -74,10 +74,13 @@ jobs: background: true run: sfw npm install -g npm - # Install the Socket CLI tool using pip and ensure it's up to date + # Install the Socket CLI tool using pip and ensure it's up to date. + # The local validate:package command maintains this pin using the latest non-yanked stable + # release that has been available on PyPI for at least 24 hours. CI runs the corresponding + # validate:package:skip-reachability command so workflow validation does not modify its checkout. - name: Install Socket CLI background: true - run: sfw pip install socketsecurity uv --upgrade + run: sfw pip install socketsecurity==2.5.5 uv --upgrade # Bring job back to sync execution by awaiting for all async jobs to finish before continuing - name: Steps - Convert Back To Synchronous Execution - Packages Updates/Setup @@ -88,4 +91,4 @@ jobs: env: SOCKET_SECURITY_API_KEY: ${{ secrets.SOCKET_REACHABILITY }} GH_API_TOKEN: ${{ secrets.GITHUB_TOKEN }} - run: socketcli --target-path $GITHUB_WORKSPACE --scm github --reach + run: socketcli --target-path $GITHUB_WORKSPACE --scm github --pr-number ${{ github.event.pull_request.number || 0 }} --reach diff --git a/package.json b/package.json index 569e616..4c7cb40 100644 --- a/package.json +++ b/package.json @@ -14,7 +14,9 @@ "test": "npm run build:coverage && vitest run", "test:watch": "vitest", "coverage": "npm run build:coverage && vitest run --coverage", - "validate:package": "npm run lint && npm run coverage && npm run build:prod && node ./scripts/validate-package.mjs", + "validate:package": "npm run update:reachability-pin && npm run validate:package:skip-reachability", + "validate:package:skip-reachability": "npm run lint && npm run coverage && npm run build:prod && node ./scripts/validate-package.mjs", + "update:reachability-pin": "node ./scripts/update-reachability-pin.mjs", "prepack": "npm run validate:package", "postinstall": "ts-patch install -s" }, diff --git a/scripts/update-reachability-pin.mjs b/scripts/update-reachability-pin.mjs new file mode 100644 index 0000000..a2b1913 --- /dev/null +++ b/scripts/update-reachability-pin.mjs @@ -0,0 +1,67 @@ +import { readFile, writeFile } from 'node:fs/promises'; +import { fileURLToPath } from 'node:url'; + +const PYPI_URL = 'https://pypi.org/pypi/socketsecurity/json'; +const RELEASE_AGE_MILLISECONDS = 24 * 60 * 60 * 1000; +const workflowPath = fileURLToPath(new URL('../.github/workflows/Security-Reachability.yml', import.meta.url)); +const versionPattern = /^\d+(?:\.\d+)*$/u; +const pinPattern = /socketsecurity==(?\d+(?:\.\d+)*)/gu; + +const compareVersions = (left, right) => { + const leftParts = left.split('.').map(Number); + const rightParts = right.split('.').map(Number); + const partCount = Math.max(leftParts.length, rightParts.length); + + for (let index = 0; index < partCount; index += 1) { + const difference = (leftParts[index] ?? 0) - (rightParts[index] ?? 0); + + if (difference !== 0) { + return difference; + } + } + + return 0; +}; + +const response = await fetch(PYPI_URL); + +if (!response.ok) { + throw new Error(`Unable to retrieve socketsecurity releases from PyPI: ${ response.status } ${ response.statusText }`); +} + +const { releases } = await response.json(); +const cutoffTime = Date.now() - RELEASE_AGE_MILLISECONDS; +const eligibleRelease = Object.entries(releases) + .filter(([version, files]) => versionPattern.test(version) && Array.isArray(files) && files.length > 0 && files.every((file) => !file.yanked)) + .map(([version, files]) => { + const mostRecentUpload = Math.max(...files.map((file) => Date.parse(file.upload_time_iso_8601))); + + return { + 'mostRecentUpload': mostRecentUpload, + 'version': version + }; + }) + .filter(({ mostRecentUpload }) => Number.isFinite(mostRecentUpload) && mostRecentUpload <= cutoffTime) + .sort((left, right) => compareVersions(right.version, left.version))[0]; + +if (!eligibleRelease) { + throw new Error('PyPI did not return a non-yanked stable socketsecurity release at least 24 hours old.'); +} + +const workflow = await readFile(workflowPath, 'utf8'); +const pins = [...workflow.matchAll(pinPattern)]; + +if (pins.length !== 1) { + throw new Error(`Expected exactly one socketsecurity pin in ${ workflowPath }, found ${ pins.length }.`); +} + +const currentVersion = pins[0].groups.version; + +if (currentVersion === eligibleRelease.version) { + process.stdout.write(`Reachability pin is current: socketsecurity==${ currentVersion }\n`); +} else { + const updatedWorkflow = workflow.replace(pinPattern, `socketsecurity==${ eligibleRelease.version }`); + + await writeFile(workflowPath, updatedWorkflow); + process.stdout.write(`Updated reachability pin: socketsecurity==${ currentVersion } -> socketsecurity==${ eligibleRelease.version }\n`); +}