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
57 changes: 50 additions & 7 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -105,8 +105,8 @@ jobs:
fail-on-severity: high
fail-on-scopes: development, runtime

workspace:
name: Workspace contract
database-contract:
name: Database contract
needs: core-release
runs-on: ubuntu-latest
steps:
Expand Down Expand Up @@ -159,6 +159,38 @@ jobs:
packages/core/src/database/database.generated.ts
packages/core/src/database/runtime-contract.generated.json

- name: Reject a moved stable release
run: >-
node scripts/resolve-core-release.mjs
--expect "${{ needs.core-release.outputs.image }}"

workspace:
name: Workspace contract
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
fetch-depth: 0

- name: Install the pinned pnpm release
uses: pnpm/action-setup@0ebf47130e4866e96fce0953f49152a61190b271 # v6.0.9
with:
version: 11.11.0

- name: Set up Node
uses: actions/setup-node@249970729cb0ef3589644e2896645e5dc5ba9c38 # v6
with:
node-version-file: package.json
cache: pnpm
registry-url: https://npm.pkg.github.com
scope: '@inkcre'

- name: Install the frozen workspace
run: pnpm install --frozen-lockfile
env:
NODE_AUTH_TOKEN: ${{ secrets.GITHUB_TOKEN }}

- name: Verify static, typed, and package contracts
run: pnpm check

Expand All @@ -181,6 +213,14 @@ jobs:
--core-package packages/core/package.json
--artifact-directory extensions/twitter/dist/client-web

- name: Build the read-only Twitter preview Release
run: >-
node scripts/verify-twitter-mf-distribution.mjs preview-release
--package extensions/twitter/package.json
--core-package packages/core/package.json
--artifact-directory extensions/twitter/dist/client-web
--output .preview-release/twitter.json

- name: Upload the exact Twitter Module Federation snapshot
id: twitter-mf-artifact
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
Expand All @@ -190,6 +230,14 @@ jobs:
if-no-files-found: error
retention-days: 7

- name: Upload the exact Twitter preview Release
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
name: twitter-mf-preview-release
path: .preview-release/twitter.json
if-no-files-found: error
retention-days: 7

- name: Record checked artifact provenance
env:
CLIENT_WEB_ARTIFACT_DIGEST: ${{ steps.client-web-artifact.outputs['artifact-digest'] }}
Expand All @@ -203,11 +251,6 @@ jobs:
echo "- Twitter MF snapshot digest: \`${TWITTER_MF_ARTIFACT_DIGEST}\`"
} >> "$GITHUB_STEP_SUMMARY"

- name: Reject a moved stable release
run: >-
node scripts/resolve-core-release.mjs
--expect "${{ needs.core-release.outputs.image }}"

- name: Upload unit-test evidence on failure
if: failure()
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
Expand Down
64 changes: 61 additions & 3 deletions .github/workflows/pages-preview.yml
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,6 @@ jobs:
identity:
name: Resolve trusted preview identity
if: >-
github.event.workflow_run.conclusion == 'success' &&
github.event.workflow_run.event == 'pull_request' &&
github.event.workflow_run.head_repository.full_name == github.repository
runs-on: ubuntu-latest
Expand All @@ -41,15 +40,50 @@ jobs:
core.setFailed('Preview artifact must come from the trusted Client checks workflow.')
return
}
if (run.conclusion !== 'success' || run.event !== 'pull_request') {
core.setFailed('Preview delivery requires a successful pull-request check run.')
if (run.status !== 'completed' || run.event !== 'pull_request') {
core.setFailed('Preview delivery requires a completed pull-request check run.')
return
}
if (run.head_repository?.full_name !== `${context.repo.owner}/${context.repo.repo}`) {
core.setFailed('Fork pull requests are not eligible for preview delivery.')
return
}

const jobs = await github.paginate(github.rest.actions.listJobsForWorkflowRun, {
owner: context.repo.owner,
repo: context.repo.repo,
run_id: runId,
filter: 'latest',
per_page: 100,
})
const workspace = jobs.find((job) => job.name === 'Workspace contract')
if (!workspace || workspace.status !== 'completed' || workspace.conclusion !== 'success') {
core.setFailed('Preview delivery requires the Workspace contract job to pass.')
return
}

const artifacts = await github.paginate(github.rest.actions.listWorkflowRunArtifacts, {
owner: context.repo.owner,
repo: context.repo.repo,
run_id: runId,
per_page: 100,
})
for (const artifactName of [
'client-web-dist',
'twitter-mf-dist',
'twitter-mf-preview-release',
]) {
const artifact = artifacts.find(
(candidate) => candidate.name === artifactName && !candidate.expired
)
if (!artifact) {
core.setFailed(
`The successful Workspace contract job produced no live ${artifactName} artifact.`
)
return
}
}

const pulls = await github.rest.repos.listPullRequestsAssociatedWithCommit({
owner: context.repo.owner,
repo: context.repo.repo,
Expand Down Expand Up @@ -106,6 +140,30 @@ jobs:
run-id: ${{ needs.identity.outputs.run_id }}
github-token: ${{ secrets.GITHUB_TOKEN }}

- name: Download the exact checked Twitter snapshot
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
with:
name: twitter-mf-dist
path: .pages-preview/twitter-mf
run-id: ${{ needs.identity.outputs.run_id }}
github-token: ${{ secrets.GITHUB_TOKEN }}

- name: Download the exact checked Twitter preview Release
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
with:
name: twitter-mf-preview-release
path: .pages-preview/twitter-release
run-id: ${{ needs.identity.outputs.run_id }}
github-token: ${{ secrets.GITHUB_TOKEN }}

- name: Assemble the read-only native Extension preview
run: >-
node controller/scripts/assemble-twitter-preview.mjs
--pages-directory .pages-preview/dist
--snapshot-directory .pages-preview/twitter-mf
--release .pages-preview/twitter-release/twitter.json
--public-origin https://preview-client-web-pr-${{ needs.identity.outputs.pull_number }}.${{ vars.CLOUDFLARE_PAGES_PROJECT }}.pages.dev

- name: Reverify pull-request identity before delivery
uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
env:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -703,5 +703,5 @@
"schema": "inkcre"
},
"revision": "peer-database-runtime-v2",
"source_revision": "34914b6deed4768237508fdb02e033655feb8073"
"source_revision": "63f57b26ed8685fa34a74516ade39e2af72218d9"
}
145 changes: 145 additions & 0 deletions scripts/assemble-twitter-preview.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,145 @@
import assert from 'node:assert/strict'
import { cp, lstat, mkdir, readFile, writeFile } from 'node:fs/promises'
import path from 'node:path'
import { fileURLToPath } from 'node:url'

const EXTENSION_NAME = 'inkcre/twitter'
const VERSION_PATTERN =
/^(?:0|[1-9]\d*)\.(?:0|[1-9]\d*)\.(?:0|[1-9]\d*)(?:-[0-9A-Za-z-]+(?:\.[0-9A-Za-z-]+)*)?$/

async function readJson(filePath) {
return JSON.parse(await readFile(filePath, 'utf8'))
}

function parseArguments(argumentList) {
const options = {}
for (let index = 0; index < argumentList.length; index += 2) {
const option = argumentList[index]
const value = argumentList[index + 1]
assert.ok(option?.startsWith('--') && value, `expected option/value near ${option ?? ''}`)
options[option.slice(2)] = value
}
return options
}

function requireOption(options, name) {
assert.ok(options[name], `--${name} is required`)
return options[name]
}

export async function assembleTwitterPreview({
pagesDirectory,
snapshotDirectory,
releasePath,
publicOrigin,
}) {
const [release, manifest, pagesIndex, snapshotStats] = await Promise.all([
readJson(releasePath),
readJson(path.join(snapshotDirectory, 'mf-manifest.json')),
lstat(path.join(pagesDirectory, 'index.html')),
lstat(snapshotDirectory),
])

assert.ok(pagesIndex.isFile(), 'Pages preview artifact must contain index.html')
assert.ok(snapshotStats.isDirectory(), 'Twitter snapshot must be a directory')
assert.equal(release.name, EXTENSION_NAME, 'preview Release must be inkcre/twitter')
assert.equal(typeof release.nickname, 'string', 'preview Release nickname is required')
assert.match(release.version, VERSION_PATTERN, 'preview Release version must be strict SemVer')
assert.equal(release.state, 'published', 'preview Release must be published')
assert.deepEqual(release.python, undefined, 'Web preview must not claim a Python Distribution')
assert.deepEqual(
release.module_federation,
{
manifest_url: `/extensions/${EXTENSION_NAME}/${release.version}/module-federation/mf-manifest.json`,
host_sdk: '@inkcre/core',
host_sdk_version: release.module_federation?.host_sdk_version,
},
'preview Release must contain exactly one native Web association'
)
assert.equal(
typeof release.module_federation.host_sdk_version,
'string',
'preview Web Host SDK range is required'
)
assert.equal(manifest.name, 'extension.twitter', 'unexpected Module Federation Remote name')
assert.equal(manifest.metaData?.publicPath, './', "preview manifest publicPath must be './'")
assert.equal(
manifest.metaData?.remoteEntry?.name,
'remoteEntry.js',
'preview manifest remote entry must be remoteEntry.js'
)
const canonicalOrigin = new URL(publicOrigin)
assert.equal(canonicalOrigin.protocol, 'https:', 'preview public origin must use HTTPS')
assert.equal(canonicalOrigin.pathname, '/', 'preview public origin must not contain a path')
assert.equal(canonicalOrigin.search, '', 'preview public origin must not contain a query')
assert.equal(canonicalOrigin.hash, '', 'preview public origin must not contain a fragment')

const releaseFile = path.join(
pagesDirectory,
'v1',
'extensions',
'inkcre',
'twitter',
'releases',
release.version
)
const distributionDirectory = path.join(
pagesDirectory,
'extensions',
'inkcre',
'twitter',
release.version,
'module-federation'
)
await Promise.all([
mkdir(path.dirname(releaseFile), { recursive: true }),
mkdir(distributionDirectory, { recursive: true }),
])
await cp(snapshotDirectory, distributionDirectory, { recursive: true, force: false })
manifest.metaData.publicPath = new URL(
`/extensions/${EXTENSION_NAME}/${release.version}/module-federation/`,
canonicalOrigin
).href
await writeFile(
path.join(distributionDirectory, 'mf-manifest.json'),
`${JSON.stringify(manifest, null, 2)}\n`
)
await writeFile(releaseFile, `${JSON.stringify(release, null, 2)}\n`)

const headersPath = path.join(pagesDirectory, '_headers')
const existingHeaders = await readFile(headersPath, 'utf8').catch(() => '')
const previewHeaders = [
'/v1/extensions/*',
' Access-Control-Allow-Origin: *',
' Cache-Control: no-store',
' Content-Type: application/json; charset=utf-8',
'/extensions/*',
' Access-Control-Allow-Origin: *',
' Cache-Control: no-store',
'',
].join('\n')
await writeFile(
headersPath,
`${existingHeaders.trim()}${existingHeaders.trim() ? '\n' : ''}${previewHeaders}`
)

return { name: release.name, version: release.version, releaseFile, distributionDirectory }
}

async function main() {
const options = parseArguments(process.argv.slice(2))
await assembleTwitterPreview({
pagesDirectory: requireOption(options, 'pages-directory'),
snapshotDirectory: requireOption(options, 'snapshot-directory'),
releasePath: requireOption(options, 'release'),
publicOrigin: requireOption(options, 'public-origin'),
})
}

const currentFile = fileURLToPath(import.meta.url)
if (process.argv[1] && path.resolve(process.argv[1]) === currentFile) {
main().catch((error) => {
console.error(`[ERROR] ${error.message}`)
process.exitCode = 1
})
}
2 changes: 1 addition & 1 deletion scripts/check-local-runtime-contract.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@ if (await exists('.node-version')) {
errors.push('.node-version must remain absent; pnpm devEngines.runtime owns Node')
}
for (const [path, expectedNodeFiles] of [
['.github/workflows/ci.yml', Array(5).fill('package.json')],
['.github/workflows/ci.yml', Array(6).fill('package.json')],
['.github/workflows/pages-cleanup.yml', ['controller/package.json']],
['.github/workflows/pages-preview.yml', ['controller/package.json']],
['.github/workflows/pages-deploy.yml', ['controller/package.json']],
Expand Down
Loading
Loading