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
5 changes: 4 additions & 1 deletion .github/workflows/ci-check.yml
Original file line number Diff line number Diff line change
@@ -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]
Expand All @@ -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:
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/ci-ready.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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]
Expand Down
10 changes: 8 additions & 2 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
12 changes: 11 additions & 1 deletion tests/preview.test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -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};
}

Expand All @@ -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 () => {
Expand Down