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
4 changes: 2 additions & 2 deletions .github/workflows/release.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -182,7 +182,7 @@ jobs:
needs: prepare
runs-on: ubuntu-24.04
environment: npm-production
timeout-minutes: 30
timeout-minutes: 90
permissions:
attestations: write
contents: read
Expand Down Expand Up @@ -320,7 +320,7 @@ jobs:

# Infra components are not pnpm workspaces. Keep their npm locks
# peer-aware so Dockerfile npm ci remains reproducible.
for directory in infra/*/; do
for directory in infra/*/ infra/uhrp-server-cloud-bucket/notifier/; do
[ -f "$directory/package.json" ] || continue
[ -f "$directory/package-lock.json" ] || continue
echo "Refreshing lockfile in $directory"
Expand Down
3 changes: 2 additions & 1 deletion docs/about/versioning.md
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,8 @@ Then inspect its first-party dependents:
3. Breaking first-party changes require one coordinated migration across all
affected dependents before publication.
4. Infrastructure manifests and locks are reconciled after a successful
cascade release; they are deployed separately as immutable images.
cascade release, including the separately deployed UHRP notifier function.
Service images and the notifier function are promoted separately.

Workspace runtime and development references use `workspace:^`; public peer
dependencies use reviewed public ranges. `scripts/check-versions.mjs` rejects
Expand Down
5 changes: 4 additions & 1 deletion docs/reference/npm-package-supply-chain.md
Original file line number Diff line number Diff line change
Expand Up @@ -76,7 +76,10 @@ build or lifecycle code, and:
No build, pack, code-generation, or version-rewrite step may run between
attestation and publication. Published npm versions are immutable. A partial
retry skips an already-published version only when its registry digest exactly
matches the staged artifact.
matches the staged artifact. Registry reconciliation polls for up to ten minutes
per newly published package; the publication job has a 90-minute overall bound.
If propagation exceeds that bound, inspect registry bytes before resuming the
same candidate. A timeout does not prove that npm rejected publication.

The machine-readable contract is
`governance/npm-package-supply-chain.json`. Repository health tests ratchet the
Expand Down
7 changes: 6 additions & 1 deletion infra/uhrp-server-cloud-bucket/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -254,7 +254,12 @@ must:

4. Deploy/replace the Cloud Run service in the bucket’s region.

5. Deploy the notifier Cloud Run service.
5. Deploy the notifier Cloud Run function on Node.js 24, matching its package
runtime contract. The release sync includes its separate manifest and npm
lockfile, so deploy the reviewed notifier source with the synchronized SDK
floor. Its advertisement requests use `Authorization: Bearer <ADMIN_TOKEN>`;
sending the token only in a JSON body is unsupported. Validate the notifier
against the staging server before promoting the same source to production.

Source pushes do not implicitly authorize a Cloud Run deployment. Promotion is
an explicit operator-owned action against a verified image digest.
Expand Down
4 changes: 2 additions & 2 deletions infra/uhrp-server-cloud-bucket/notifier/deployProd.sh
Original file line number Diff line number Diff line change
Expand Up @@ -9,12 +9,12 @@ echo "Bucket $GCP_BUCKET_NAME is in region: $BUCKET_REGION"

gcloud functions deploy prodNotifier \
--gen2 \
--runtime=nodejs22 \
--runtime=nodejs24 \
--env-vars-file=prod.functions.env.yaml \
--entry-point=notifier \
--timeout=540 \
--region=$(echo "$BUCKET_REGION" | tr '[:upper:]' '[:lower:]') \
--trigger-event=google.storage.object.finalize \
--trigger-resource=$GCP_BUCKET_NAME \
--memory=4096 \
--source .
--source .
4 changes: 2 additions & 2 deletions infra/uhrp-server-cloud-bucket/notifier/deployStaging.sh
Original file line number Diff line number Diff line change
Expand Up @@ -9,12 +9,12 @@ echo "Bucket $GCP_BUCKET_NAME is in region: $BUCKET_REGION"

gcloud functions deploy stagingNotifier \
--gen2 \
--runtime=nodejs22 \
--runtime=nodejs24 \
--env-vars-file=staging.functions.env.yaml \
--entry-point=notifier \
--timeout=540 \
--region=$(echo "$BUCKET_REGION" | tr '[:upper:]' '[:lower:]') \
--trigger-event=google.storage.object.finalize \
--trigger-resource=$GCP_BUCKET_NAME \
--memory=4096 \
--source .
--source .
4 changes: 3 additions & 1 deletion scripts/package-release-artifacts.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,9 @@ const EXPECTED_WORKFLOW = '.github/workflows/release.yaml'
const POLICY_PATH = path.join(REPOSITORY_ROOT, 'governance/npm-package-supply-chain.json')
const COMMAND_TIMEOUT_MS = 10 * 60_000
const MAX_BUFFER_BYTES = 64 * 1024 * 1024
const REGISTRY_RETRY_ATTEMPTS = 20
// npm can acknowledge publication before its public metadata exposes the version.
// Keep reconciliation bounded while allowing the observed propagation delay.
const REGISTRY_RETRY_ATTEMPTS = 41
const REGISTRY_RETRY_DELAY_MS = 15_000
const URL_NAMESPACE_UUID = '6ba7b811-9dad-11d1-80b4-00c04fd430c8'
const PACKED_MANIFEST_DEPENDENCY_FIELDS = [
Expand Down
14 changes: 9 additions & 5 deletions scripts/sync-versions.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
* dependency reference (dependencies, devDependencies, peerDependencies)
* so that they point at the current workspace version.
*
* Also walks ./infra/* package.json files (which are NOT in the pnpm
* Also walks ./infra/* and the nested UHRP notifier package.json files (NOT in the pnpm
* workspace) and rewrites their @bsv/* dependency ranges to track the
* latest workspace versions. When an infra component's deps change, its
* own version is patch-bumped so the infra-release workflow rebuilds
Expand Down Expand Up @@ -124,7 +124,7 @@ console.log(
`\n${DRY_RUN ? '[DRY RUN] Would update' : 'Updated'} ${totalChanges} cross-package references`
)

// --- 3. Sync ./infra/* (not part of pnpm workspace) ---
// --- 3. Sync standalone infrastructure (not part of pnpm workspace) ---
//
// Infra components consume workspace packages from the npm registry, not via
// `workspace:*`. After a publish, their `^X.Y.Z` ranges go stale relative to
Expand Down Expand Up @@ -180,9 +180,13 @@ if (!WORKSPACE_ONLY) {
typeof error === 'object' && error !== null && 'code' in error && error.code === 'ENOENT'
if (!missing) throw error
}
for (const entry of entries) {
if (!entry.isDirectory()) continue
const componentDir = join(INFRA_DIR, entry.name)
// Only owned service roots and the separately deployed notifier participate.
// Never recurse into node_modules or arbitrary nested examples.
const componentDirs = entries
.filter(entry => entry.isDirectory())
.map(entry => join(INFRA_DIR, entry.name))
componentDirs.push(join(INFRA_DIR, 'uhrp-server-cloud-bucket', 'notifier'))
for (const componentDir of componentDirs) {
const jsonPath = join(componentDir, 'package.json')
const raw = readUtf8FileIfExists(jsonPath)
if (raw === undefined) continue
Expand Down
77 changes: 77 additions & 0 deletions scripts/sync-versions.test.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
import assert from 'node:assert/strict'
import { execFileSync } from 'node:child_process'
import { copyFileSync, mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from 'node:fs'
import { tmpdir } from 'node:os'
import { dirname, join } from 'node:path'
import test from 'node:test'
import { fileURLToPath } from 'node:url'

const scriptDirectory = dirname(fileURLToPath(import.meta.url))

test('release sync covers the separately deployed notifier without traversing arbitrary children', () => {
const root = mkdtempSync(join(tmpdir(), 'ts-stack-release-sync-'))
const writeJson = (relative, value) => {
const file = join(root, relative)
mkdirSync(dirname(file), { recursive: true })
writeFileSync(file, `${JSON.stringify(value, null, 2)}\n`)
}
const readJson = relative => JSON.parse(readFileSync(join(root, relative), 'utf8'))
const service = 'infra/uhrp-server-cloud-bucket/package.json'
const notifier = 'infra/uhrp-server-cloud-bucket/notifier/package.json'
const ignored = 'infra/uhrp-server-cloud-bucket/examples/package.json'
try {
mkdirSync(join(root, 'scripts'))
for (const file of ['sync-versions.mjs', 'file-system.mjs']) {
copyFileSync(join(scriptDirectory, file), join(root, 'scripts', file))
}
writeJson('package.json', { name: 'release-sync-fixture', private: true })
writeFileSync(join(root, 'pnpm-workspace.yaml'), "packages:\n - 'packages/*'\n")
writeJson('packages/sdk/package.json', { name: '@bsv/sdk', version: '2.8.0' })
// Repository-health CI intentionally runs before package-manager setup.
// Supply only the unchanged workspace-discovery boundary to this fixture.
const bin = join(root, 'bin')
mkdirSync(bin)
const workspaceListing = JSON.stringify([
{ name: '@bsv/sdk', version: '2.8.0', path: join(root, 'packages/sdk') }
])
writeFileSync(
join(bin, 'pnpm'),
`#!/usr/bin/env node\nprocess.stdout.write(${JSON.stringify(workspaceListing)})\n`,
{ mode: 0o700 }
)
const oldManifest = {
name: 'standalone-consumer',
version: '1.0.0',
dependencies: { '@bsv/sdk': '^2.1.9', axios: '^1.18.1' }
}
for (const path of [service, notifier, ignored]) writeJson(path, oldManifest)
const run = (...args) =>
execFileSync(process.execPath, [join(root, 'scripts/sync-versions.mjs'), ...args], {
cwd: root,
encoding: 'utf8',
env: { ...process.env, PATH: `${bin}:${dirname(process.execPath)}` }
})

assert.match(
run('--dry-run'),
/Would update 2 infra dep reference\(s\) across 2 component\(s\)/
)
for (const path of [service, notifier, ignored]) assert.deepEqual(readJson(path), oldManifest)
run('--workspace-only')
assert.deepEqual(readJson(notifier), oldManifest)

run()
for (const path of [service, notifier]) {
assert.deepEqual(readJson(path), {
...oldManifest,
version: '1.0.1',
dependencies: { '@bsv/sdk': '^2.8.0', axios: '^1.18.1' }
})
}
assert.deepEqual(readJson(ignored), oldManifest)
assert.match(run(), /Updated 0 infra dep reference\(s\) across 0 component\(s\)/)
assert.equal(readJson(notifier).version, '1.0.1')
} finally {
rmSync(root, { recursive: true, force: true })
}
})
Loading