diff --git a/.github/workflows/ci-check.yml b/.github/workflows/ci-check.yml index 8a34fc9..ec541ad 100644 --- a/.github/workflows/ci-check.yml +++ b/.github/workflows/ci-check.yml @@ -1,6 +1,9 @@ name: Check on: + workflow_dispatch: + push: + branches: [main] pull_request: branches: [main] types: [opened, synchronize, reopened, ready_for_review, converted_to_draft] @@ -9,7 +12,7 @@ permissions: contents: read concurrency: - group: check-${{ github.event.pull_request.number }}-${{ github.event.pull_request.draft && 'draft' || 'ready' }} + group: check-${{ github.event.pull_request.number || github.ref }}-${{ github.event.pull_request.draft && 'draft' || 'ready' }} cancel-in-progress: true jobs: diff --git a/.github/workflows/ci-ready.yml b/.github/workflows/ci-ready.yml index 4d108f0..09bd102 100644 --- a/.github/workflows/ci-ready.yml +++ b/.github/workflows/ci-ready.yml @@ -3,7 +3,7 @@ name: Ready on: pull_request_target: branches: [main] - types: [opened, synchronize, reopened, ready_for_review, converted_to_draft, labeled, unlabeled] + types: [opened, synchronize, reopened, ready_for_review, converted_to_draft] workflow_run: workflows: [Check] types: [completed] diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 0c0c43c..6397864 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -192,9 +192,15 @@ jobs: if (url.protocol !== 'https:' || url.username || url.password || !(built || skipped)) { throw new Error(`${name} did not return this PR's Netlify preview URL.`); } - if (!targets.length) { urls[name] = origin; return; } - const target = selected.find(site => site.context === name); const revision = await revisionAt(origin); + if (!targets.length) { + if (revision === sha) { + urls[name] = origin; + revisions[origin] = revision; + } + return; + } + const target = selected.find(site => site.context === name); if (built ? revision === sha : compatible(target, revision)) { urls[name] = origin; revisions[origin] = revision; diff --git a/README.md b/README.md index d3dfaa2..8044b90 100644 --- a/README.md +++ b/README.md @@ -15,6 +15,8 @@ Fulldev, astro-template and vite-template expose `fix`, `check`, `build` and `te These callers use the Check workflow's optional `test-command` to run unit and service tests after source checks with one installation. Their Test caller disables its separate unit job. A separate Preview job waits for the selected PR revision’s deployments before Web installs dependencies or acquires its shared-account lock. Waiting does not consume the Web test budget. +Single-site callers using `preview-checks` also require `/.well-known/build.json` to identify the tested PR head. Every selected revision is checked again after Web waits for its shared-account lock. + Monorepos can give Test a `deployments` array of Netlify `context`, production `url`, repository-relative `config` and optional `base`. Preview selects targets by running each native `build.ignore` against the cumulative PR diff. Changes in `tests/` or `.github/workflows/test.yml` select every target. Netlify uses the same ignore rule against its cached revision. Skipped previews are reusable only when `/.well-known/build.json` proves an ancestor with unchanged build inputs. Static websites may explicitly enable `production-fallback: true` for a skipped first preview; signed-in apps always require an isolated preview. `QA_DEPLOY_URLS` contains only selected targets, and repositories must exclude absent targets from their suites. An explicitly empty selection skips Web and passes Test; missing or failed selection never does. After a push to main, Check runs source checks and unit/service tests again, Netlify runs the same build command, and Test live waits for the published revision before safe live HTTP/browser tests. Its `deployments` input lists HTTPS origins and their repository-relative Netlify config/base paths. Each build publishes `/.well-known/build.json` with its `COMMIT_REF`. For an older production deploy, Test live accepts it only if it is an ancestor of the requested revision and that site's existing Netlify `build.ignore` command confirms unchanged inputs. No Netlify token or second build is needed. diff --git a/tests/preview.test.mjs b/tests/preview.test.mjs index 2280c25..5754fe0 100644 --- a/tests/preview.test.mjs +++ b/tests/preview.test.mjs @@ -59,7 +59,8 @@ async function scenario(options = {}) { {info() {}, setOutput: (name, value) => { outputs[name] = value; }}, {env: {...process.env, PREVIEW_CHECKS: JSON.stringify(names), DEPLOYMENTS: JSON.stringify(options.targets ?? []), GITHUB_WORKSPACE: options.root}}, {now: () => clock}, callback => { clock += 60 * 1000; callback(); }, - createRequire(import.meta.url), async url => ({ok: true, json: async () => ({revision: options.revisions?.[new URL(url).origin] ?? options.head})})); + createRequire(import.meta.url), async url => ({ok: !options.missingMarker, + json: async () => ({revision: options.revisions?.[new URL(url).origin] ?? pull.head.sha})})); return {outputs, refs}; } @@ -69,6 +70,15 @@ test('URL tests use previews associated with the exact PR head', async () => { assert.equal(outputs.sha, 'tested-head'); assert.equal(outputs['base-url'], 'https://deploy-preview-7--app.netlify.app'); assert.equal(Object.keys(JSON.parse(outputs['deploy-urls'])).length, 2); + assert.deepEqual(JSON.parse(outputs['deploy-revisions']), { + 'https://deploy-preview-7--app.netlify.app': 'tested-head', + 'https://deploy-preview-7--site.netlify.app': 'tested-head', + }); +}); + +test('single-site previews require the served build to match the PR head', async () => { + await assert.rejects(scenario({revisions: {[appAlias]: 'another-head'}}), /within 12 minutes/); + await assert.rejects(scenario({missingMarker: true}), /within 12 minutes/); }); test('failed, absent and pending previews cannot become passing URL tests', async () => {