diff --git a/.github/workflows/npm-publish-from-pr.yml b/.github/workflows/npm-publish-from-pr.yml index 5b9779f..6719481 100644 --- a/.github/workflows/npm-publish-from-pr.yml +++ b/.github/workflows/npm-publish-from-pr.yml @@ -3,26 +3,19 @@ name: npm publish from PR on: workflow_dispatch: inputs: -# source: -# description: 'source ref: `google/zx/main/0cba...f9`' repo: description: 'source gh repo, like `google/zx`' -# branch: -# description: 'target branch' commit: description: 'commit id' -# cmd: -# description: 'build cmd' -# default: 'npm ci && npm run build' jobs: build: runs-on: ubuntu-latest steps: - - name: Use Node.js 23 + - name: Use Node.js 22 uses: actions/setup-node@v4 with: - node-version: 23 + node-version: 22 - name: Checkout uses: actions/checkout@v4 @@ -67,5 +60,8 @@ jobs: NPM_TOKEN: ${{ secrets.NPM_TOKEN }} - run: | + node -e "const fs = require('fs'); fs.writeFileSync('./README.md', process.env.NOTICE + fs.readFileSync('./README.md'))" npm version $(node --eval="process.stdout.write(require('./package.json').version)")-pr.${{ github.run_id }} --no-git-tag-version npm publish --provenance --access=public --no-git-tag-version --tag pr + env: + NOTICE: "> ⚠ **This snapshot was built from the external source** \n> repo: ${{ github.event.inputs.repo }} \n> commit: ${{ github.event.inputs.commit }}\n" diff --git a/package.json b/package.json index b95ba24..628f005 100644 --- a/package.json +++ b/package.json @@ -48,8 +48,7 @@ "test:unit": "c8 -r lcov -r text -o target/coverage -x src/scripts -x src/test -x target node --loader ts-node/esm --experimental-specifier-resolution=node src/scripts/test.mjs", "test:smoke:esm": "node ./src/test/smoke/invoke.test.mjs", "test:smoke:cjs": "node src/test/smoke/invoke.test.cjs", - "publish:draft": "npm run build && npm publish --no-git-tag-version", - "manual:test:build-from-remote": "NODE_OPTIONS='--experimental-strip-types' npx zx@latest ./src/scripts/build-from-remote.mts --mode=test" + "publish:draft": "npm run build && npm publish --no-git-tag-version" }, "repository": { "type": "git", diff --git a/src/scripts/build-from-remote.mts b/src/scripts/build-from-remote.mts deleted file mode 100644 index 0fc18c0..0000000 --- a/src/scripts/build-from-remote.mts +++ /dev/null @@ -1,205 +0,0 @@ -#!/usr/bin/env zx - -import assert from 'node:assert' -import process from 'node:process' -import { describe, it } from 'node:test' -// import { $, tempdir, argv } from 'zx' - -const MODE = argv.mode || 'run' -const SECRETS = ['NPM_TOKEN', 'GH_TOKEN', 'GITHUB_TOKEN', 'AUTH_TOKEN'] -const GH_URL = 'https://github.com' - -export interface TContext { - cwd?: string - cmd?: string - repo: string, - branch: string - commit: string - output?: string -} - -export const protect = (env = process.env) => { - if (SECRETS.some(k => k in env)) throw new Error('Credentials should not be observable from the build step') -} - -export const createContext = (av: Record = argv, env = process.env) => { - const input = { - cwd: process.cwd(), - ...JSON.parse(av.ctx || env.CTX || '{}'), - ...av, - } - - const source = input.source || `${input.repo}/${input.branch}/${input.commit}` - const sourceRef = parseSourceRef(source) - const ctx: TContext = { - ...input, - ...sourceRef - } - - return ctx -} - -export const parseSourceRef = (ref: string): Pick => { - const re = /^(?:https:\/\/:)?([\w-]+\/[\w-]+)\/([\w-]+(?:\/[\w-]+)*)\/([\da-f]{8,40})/i - const [, repo, branch, commit] = re.exec(ref) || [] - - if (!repo) throw new Error('Invalid source ref') - - return { - repo, - branch, - commit - } -} - -export const fetchSource = async ({repo, branch, commit, cwd}: Pick) => { - const repoUrl = `${GH_URL}/${repo}` - const $$ = $({cwd, quiet: true}) - await $$`git clone -b ${branch} --depth=1 ${repoUrl} .` - - const _commit = (await $$`git rev-parse HEAD`).toString().trim() - if (!_commit.startsWith(commit)) throw new Error(`Commit hash mismatch: ${commit} !== ${_commit} at remote ${branch} HEAD`) -} - -export const buildSource = async ({cwd, cmd}: Pick) => - cmd ? $({cwd})`${cmd}` : $({cwd})`exit 0` - -export const buildFromRemote = async (av = argv, env = process.env)=> { - protect() - const ctx = createContext(av, env) - await fetchSource(ctx) - await buildSource(ctx) - ctx.output && await fs.outputJson(ctx) -} - -;(async() => { - try { - if (MODE === 'run') { - await run() - process.exit(0) - } else if (MODE === 'test') { - test() - } else { - throw new Error(`unknown mode: ${MODE}. Only 'test' & 'run' values are supported`) - } - } catch (e: unknown) { - console.error((e as Error).message) - process.exit(1) - } -})() - -async function run () { - await buildFromRemote() -} - -function test(){ - describe('build-from-remote', () => { - describe('createContext', () => { - it('inits script context', () => { - const source = 'google/zx/main/0cba54884f3084af1674118ef6299302d82daaf9' - const ref = parseSourceRef(source) - assert.deepEqual(createContext({cwd: 'foo', source}), {cwd: 'foo', ...ref, source}) - assert.deepEqual(createContext({}, {CTX: JSON.stringify({cwd: 'foo', source})}), {cwd: 'foo', ...ref, source}) - }) - it('raises an error on empty source', () => { - try { - createContext({}, {}) - } catch (e) { - assert.equal((e as Error).message, 'Invalid source ref') - } - }) - it('raises an error on invalid commit hash', () => { - try { - createContext({ - repo: 'google/zx', - branch: 'main', - commit: '0cba' - }, {}) - } catch (e) { - assert.equal((e as Error).message, 'Invalid source ref') - } - }) - }) - - describe('protect', () => { - it('raises an error if secrets are exposed', () => { - try { - protect({ NPM_TOKEN: 'Foo' }) - } catch (e) { - assert.equal((e as Error).message, 'Credentials should not be observable from the build step') - } - }) - - it('does nothing otherwise', () => { - protect({}) - }) - }) - - describe('parseSourceRef()', () => { - it('parses code reference', () => { - const ref = 'google/zx/main/0cba54884f3084af1674118ef6299302d82daaf9' - const repoCtx = parseSourceRef(ref) - - assert.deepEqual(repoCtx, { - repo: 'google/zx', - branch: 'main', - commit: '0cba54884f3084af1674118ef6299302d82daaf9' - }) - }) - }) - - describe('fetchSource()', () => { - it('clones repo', async () => { - const commitId = (await $`git ls-remote git@github.com:google/zx.git refs/heads/main`).toString().trim() - const source = `google/zx/main/${commitId}` - const ref = parseSourceRef(source) - const cwd = tempdir() - const ctx = {...ref, cwd} - await fetchSource(ctx) - }) - - it('raises an error on commit id mismatch', async () => { - const source = 'google/zx/main/63ceddb2a2ae74072190683c61c4563b52aef356' - const ref = parseSourceRef(source) - const cwd = tempdir() - const ctx = {...ref, cwd} - - try { - await fetchSource(ctx) - } catch (e: unknown) { - assert.match((e as Error).message, /Commit hash mismatch: 63ceddb2a2ae74072190683c61c4563b52aef356 !== \w{40} at remote main HEAD/) - } - }) - - it('raises an error if source does not exist', async () => { - const source = 'google/zx/foobar/63ceddb2a2ae74072190683c61c4563b52aef356' - const ref = parseSourceRef(source) - const cwd = tempdir() - const ctx = {...ref, cwd} - - try { - await fetchSource(ctx) - } catch (e: unknown) { - assert.match((e as Error).message, /Could not find remote branch foobar to clone/) - } - }) - }) - - describe('build', () => { - it('invokes build cmd if specified', async () => { - const cwd = tempdir() - const result = await buildSource({ - cwd, - cmd: 'pwd' - }) - assert.ok(result.stdout.trim().endsWith(cwd)) - }) - - it('triggers exit 0 otherwise', async () => { - const cwd = tempdir() - const result = await buildSource({ cwd }) - assert.equal(result.stdout.trim(), '') - }) - }) - }) -}