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
2 changes: 1 addition & 1 deletion .github/workflows/check.yml
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ jobs:
check:
name: Check
if: ${{ !github.event.pull_request.draft }}
runs-on: ${{ inputs.runner }}
runs-on: ${{ vars.CI_RUNNER || (github.event.repository.private == false && 'ubuntu-24.04') || inputs.runner }}
timeout-minutes: 15
env:
pnpm_config_ignore_scripts: "true"
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/ready.yml
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ concurrency:
jobs:
ready:
name: Ready
runs-on: ${{ inputs.runner }}
runs-on: ${{ vars.CI_RUNNER || (github.event.repository.private == false && 'ubuntu-24.04') || inputs.runner }}
timeout-minutes: 3
steps:
- name: Create merge token for repositories with release workflows
Expand Down
18 changes: 17 additions & 1 deletion .github/workflows/test-live.yml
Original file line number Diff line number Diff line change
Expand Up @@ -23,14 +23,19 @@ on:
browser:
type: boolean
default: true
secrets:
live-test-email:
required: false
live-test-password:
required: false

permissions:
contents: read

jobs:
live:
name: Live
runs-on: ${{ inputs.runner }}
runs-on: ${{ vars.CI_RUNNER || (github.event.repository.private == false && 'ubuntu-24.04') || inputs.runner }}
timeout-minutes: 30
env:
pnpm_config_ignore_scripts: "true"
Expand All @@ -43,6 +48,14 @@ jobs:
ref: ${{ inputs.revision }}
fetch-depth: 0
persist-credentials: false
- name: Confirm the revision belongs to the default branch
env:
REVISION: ${{ inputs.revision }}
DEFAULT_BRANCH: ${{ github.event.repository.default_branch }}
shell: bash
run: |
[[ "$REVISION" =~ ^[0-9a-f]{40}$ ]]
git merge-base --is-ancestor "$REVISION" "refs/remotes/origin/$DEFAULT_BRANCH"
- uses: voidzero-dev/setup-vp@68f39c78aaf39be468cabd2afb09c162574c84af # v1.21.2
with:
version: "1.1.0"
Expand Down Expand Up @@ -130,6 +143,9 @@ jobs:
run: vp run test:http
- name: Safe live browser smoke
if: ${{ inputs.browser }}
env:
LIVE_TEST_EMAIL: ${{ secrets.live-test-email }}
LIVE_TEST_PASSWORD: ${{ secrets.live-test-password }}
# A virtual display on Linux, for repositories that run a browser headed.
shell: bash
run: |
Expand Down
8 changes: 4 additions & 4 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@ jobs:
unit:
name: Unit
if: ${{ !github.event.pull_request.draft && inputs.unit }}
runs-on: ${{ inputs.runner }}
runs-on: ${{ vars.CI_RUNNER || (github.event.repository.private == false && 'ubuntu-24.04') || inputs.runner }}
timeout-minutes: 20
env:
pnpm_config_ignore_scripts: "true"
Expand All @@ -71,7 +71,7 @@ jobs:
preview:
name: Preview
if: ${{ !github.event.pull_request.draft && (inputs.http || inputs.browser) }}
runs-on: ${{ inputs.runner }}
runs-on: ${{ vars.CI_RUNNER || (github.event.repository.private == false && 'ubuntu-24.04') || inputs.runner }}
timeout-minutes: 15
outputs:
base-url: ${{ steps.preview.outputs.base-url }}
Expand Down Expand Up @@ -231,7 +231,7 @@ jobs:
name: Web
needs: preview
if: ${{ !github.event.pull_request.draft && needs.preview.outputs.web-enabled == 'true' }}
runs-on: ${{ inputs.runner }}
runs-on: ${{ vars.CI_RUNNER || (github.event.repository.private == false && 'ubuntu-24.04') || inputs.runner }}
timeout-minutes: ${{ inputs.web-timeout-minutes }}
# Signed-in tests can share preview accounts across browser targets and PRs.
concurrency:
Expand Down Expand Up @@ -314,7 +314,7 @@ jobs:
name: Test
if: ${{ always() && !github.event.pull_request.draft }}
needs: [unit, preview, web]
runs-on: ${{ inputs.runner }}
runs-on: ${{ vars.CI_RUNNER || (github.event.repository.private == false && 'ubuntu-24.04') || inputs.runner }}
timeout-minutes: 2
steps:
- name: Require every enabled category
Expand Down
4 changes: 3 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@ Organization-wide GitHub defaults and reusable workflows. Repositories own their

Callers pin a full commit SHA from `main` after the shared workflow PR is merged.

Shared Linux jobs use GitHub's `ubuntu-24.04` for public repositories and the caller's Blacksmith runner for private repositories. To switch a project, set the repository Actions variable `CI_RUNNER` to `ubuntu-24.04` or a Blacksmith Linux runner label in GitHub's Settings → Secrets and variables → Actions → Variables. Clearing it restores the default. Commands, checks and browser coverage stay the same; native macOS jobs keep their own runners. This requires callers pinned to a revision that supports `CI_RUNNER`.

New projects take their callers from [astro-template](https://github.com/fulldotdev/astro-template) or [vite-template](https://github.com/fulldotdev/vite-template).

- **Explore** experiments locally; **Implement** builds the requested change locally.
Expand All @@ -27,6 +29,6 @@ Repositories with `test.yml` require `Test / Test` through the existing `Review`

The Ready caller grants `actions: read` so the gate reads the current Test workflow attempt and its jobs, including reruns.

Organization rules require `Check / Check` and `Review`, without requiring synchronization with main. Netlify requirements are managed centrally for repositories with those sites. Live URL tests run after deployment, outside the build command; signed-in tests and writes stay on previews.
Organization rules require `Check / Check` and `Review`, without requiring synchronization with main. Netlify requirements are managed centrally for repositories with those sites. Live URL tests run after deployment, outside the build command. Product scenarios and fixture writes stay on previews. A repository may pass `live-test-email` and `live-test-password` only from its trusted main workflow for one ordinary login/read/sign-out smoke with its own limited account; browser recordings and signed-in artifacts stay off.

Keep repository auto-merge disabled except for CloudCannon publishers. CloudCannon's Publish button keeps automatic publication for same-repository `preview` PRs created by `cloudcannon[bot]`, with all required checks and previews. Code PRs are merged by their agent.
34 changes: 32 additions & 2 deletions tests/production.test.mjs
Original file line number Diff line number Diff line change
@@ -1,9 +1,39 @@
import assert from 'node:assert/strict';
import {readFileSync} from 'node:fs';
import {spawnSync} from 'node:child_process';
import {mkdtempSync, readFileSync, rmSync} from 'node:fs';
import {execFileSync, spawnSync} from 'node:child_process';
import {tmpdir} from 'node:os';
import {join} from 'node:path';
import test from 'node:test';

test('live credentials cannot reach a feature commit or an invalid revision', () => {
const guard = readFileSync(new URL('../.github/workflows/test-live.yml', import.meta.url), 'utf8')
.split(' - name: Confirm the revision belongs to the default branch\n')[1]
.split(' run: |\n')[1].split('\n - uses:')[0]
.split('\n').map(line => line.slice(10)).join('\n');
const cwd = mkdtempSync(join(tmpdir(), 'production-revision-'));
const git = (...args) => execFileSync('git', args, {cwd, encoding: 'utf8', stdio: ['ignore', 'pipe', 'pipe']}).trim();
try {
git('init', '--initial-branch=main');
const commit = message => git('-c', 'user.name=Test', '-c', 'user.email=test@example.test', 'commit', '--allow-empty', '-m', message);
commit('production');
const main = git('rev-parse', 'HEAD');
git('update-ref', 'refs/remotes/origin/main', main);
commit('unmerged feature');
const feature = git('rev-parse', 'HEAD');
const check = revision => spawnSync('bash', ['-e', '-o', 'pipefail', '-c', guard], {
cwd, env: {...process.env, REVISION: revision, DEFAULT_BRANCH: 'main'}, encoding: 'utf8',
});
assert.equal(check(main).status, 0);
assert.notEqual(check(feature).status, 0);
assert.notEqual(check('HEAD').status, 0);
assert.notEqual(check('invalid; exit 0').status, 0);
} finally {
rmSync(cwd, {recursive: true, force: true});
}
});

const source = readFileSync(new URL('../.github/workflows/test-live.yml', import.meta.url), 'utf8')
.split(' - name: Wait for the published production revision\n')[1]
.split(' run: |\n')[1].split('\n - name: Safe live HTTP smoke')[0]
.split('\n').map(line => line.slice(10)).join('\n').split('deadline =')[0];

Expand Down