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
41 changes: 41 additions & 0 deletions .github/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
CI runs the full Lua suite for every PR targeting `master`. The shared
`actions/validate` action records the checked-out Git tree in a small artifact
named with the tree hash and run attempt. This identifies what was tested; only
a successful `Test.yml` workflow run makes it a passing result.

On a push to `master`, validation locates the merged PR's latest CI run. It
reuses that run only if its recorded tree matches the new `master` tree exactly.
This works across merge, squash, and rebase methods when the resulting files
are identical. Direct pushes, changed merge results, and missing or expired
artifacts fall back to the full suite.

On a `v*` tag, release validation looks for the latest `master` CI run for the
exact tagged commit, including annotated tags. It requires the same tree proof
and a successful conclusion. A queued or running CI run is awaited for up to
70 minutes before falling back to tests. Unavailable API results, cancelled
runs, and missing evidence also fall back to tests. A known failed validation
for the same source blocks progression; fix or rerun the failed CI run first.
Older successful runs never override a newer matching failure.

Successful fallback tests let the workflow continue. Each validation attempt
records its own source, so a master fallback can be reused by a subsequent tag.
Artifacts are retained for 90 days; older releases can still proceed by testing
again. Existing CI runs without validation artifacts also require fresh tests.
Recording an artifact is best effort: if uploading it fails, validation still
proceeds, and later stages fall back to tests when they cannot find the record.

Every reused result emits a notice, an explicit skipped-suite step, and a
workflow summary explaining the skip, with the matching tree, source run URL,
and attempt number. Fallback and blocked decisions also explain their reason
in the summary.

Release packages are built on Linux and macOS, then extracted into a temporary
directory and smoke-tested (version, help, obfuscation, and execution using the
bundled runtime). Publishing requires both packages and their smoke tests to
succeed. The normal PR → merge → tag → release path runs the full suite once.

Run the validation policy tests locally with:

```sh
node --test scripts/ci/reuse-validation.test.cjs
```
57 changes: 57 additions & 0 deletions .github/actions/validate/action.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
name: Validate Lua suite
description: Reuse verified CI results for the same files, or run the full suite.
runs:
using: composite
steps:
- name: Check validation policy
shell: bash
run: node --test scripts/ci/reuse-validation.test.cjs
- name: Identify checked-out files
id: source
shell: bash
run: |
echo "commit=$(git rev-parse HEAD)" >> "$GITHUB_OUTPUT"
echo "tree=$(git rev-parse HEAD^{tree})" >> "$GITHUB_OUTPUT"
mkdir -p "$RUNNER_TEMP/lua-validation"
git rev-parse HEAD HEAD^{tree} > "$RUNNER_TEMP/lua-validation/source.txt"
# Record what this attempt tests, including failed attempts. Consumers must
# also verify the workflow's conclusion before treating this as a pass.
- name: Record validation source
uses: actions/upload-artifact@v4
# An artifact outage must not prevent testing. Later stages will test
# again if this run has no source record they can verify.
continue-on-error: true
with:
name: lua-validation-${{ steps.source.outputs.tree }}-attempt-${{ github.run_attempt }}
path: ${{ runner.temp }}/lua-validation/source.txt
retention-days: 90
if-no-files-found: error
- name: Look for existing validation
id: validation
uses: actions/github-script@v7
env:
VALIDATION_COMMIT: ${{ steps.source.outputs.commit }}
VALIDATION_TREE: ${{ steps.source.outputs.tree }}
with:
script: |
const validate = require(`${process.env.GITHUB_WORKSPACE}/scripts/ci/reuse-validation.cjs`);
const result = await validate({ github, context, core });
core.info(result.reason);
core.setOutput('reuse', result.decision === 'reuse' ? 'true' : 'false');
core.setOutput('reason', result.reason);
const outcome = { reuse: 'Full Lua suite skipped', test: 'Full Lua suite required', block: 'Validation blocked' }[result.decision];
if (result.decision === 'reuse') core.notice(`${outcome}: ${result.reason}`);
await core.summary.addHeading(outcome).addRaw(result.reason).addEOL()
.addRaw(`Checked-out commit: ${process.env.VALIDATION_COMMIT}`).addEOL()
.addRaw(`Git tree: ${process.env.VALIDATION_TREE}`).addEOL().write();
if (result.decision === 'block') core.setFailed(result.reason);
- name: Full Lua suite skipped — reused passing validation
if: steps.validation.outputs.reuse == 'true'
shell: bash
env:
SKIP_REASON: ${{ steps.validation.outputs.reason }}
run: printf 'Full Lua suite skipped. %s\n' "$SKIP_REASON"
- name: Build and run tests
if: steps.validation.outputs.reuse != 'true'
shell: bash
run: bash scripts/run-tests.sh -b -n 10 --ci
11 changes: 9 additions & 2 deletions .github/workflows/Test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -7,11 +7,18 @@ on:
branches:
- master

permissions:
contents: read
actions: read
pull-requests: read

jobs:
test-docker:
runs-on: ubuntu-latest
# Allow waiting for an existing run, followed by the full fallback suite.
timeout-minutes: 140
steps:
- name: Checkout repo
uses: actions/checkout@v4
- name: Build and run tests
run: bash scripts/run-tests.sh -b -n 10 --ci
- name: Validate Lua suite
uses: ./.github/actions/validate
17 changes: 13 additions & 4 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -6,16 +6,18 @@ on:
- "v*"

permissions:
contents: write
contents: read
actions: read

jobs:
test:
name: Lua tests
name: Validate release source
runs-on: ubuntu-latest
timeout-minutes: 140
steps:
- uses: actions/checkout@v4
- name: Build and run tests
run: bash scripts/run-tests.sh -b -n 10 --ci
- name: Validate Lua suite
uses: ./.github/actions/validate

package:
name: Package (${{ matrix.os_name }})
Expand All @@ -40,6 +42,10 @@ jobs:
run: brew install luajit
- name: Build package
run: PROMETHEUS_BUNDLED_LUA="$(command -v luajit)" ./scripts/release/package.sh "${{ matrix.os_name }}" "${GITHUB_REF_NAME}"
- name: Smoke-test release archive
env:
RELEASE_OS: ${{ matrix.os_name }}
run: bash scripts/release/smoke-test.sh "$RELEASE_OS" "$GITHUB_REF_NAME"
- name: Upload artifact
uses: actions/upload-artifact@v4
with:
Expand All @@ -50,10 +56,13 @@ jobs:
name: Publish release
runs-on: ubuntu-latest
needs: package
permissions:
contents: write
steps:
- uses: actions/download-artifact@v4
with:
path: dist
pattern: prometheus-lua-*
- name: Move release assets
run: |
mkdir -p dist/out
Expand Down
81 changes: 81 additions & 0 deletions scripts/ci/reuse-validation.cjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
// A matching artifact identifies the checked-out Git tree, not a passing test.
// Only the latest attempt of a successful Test.yml run can supply a pass.
module.exports = async function reuseValidation({ github, context, core,
commit = process.env.VALIDATION_COMMIT,
tree = process.env.VALIDATION_TREE,
sleep = ms => new Promise(resolve => setTimeout(resolve, ms)),
now = Date.now,
waitMs = 70 * 60 * 1000,
}) {
const result = (decision, reason) => ({ decision, reason });
if (context.eventName === 'pull_request') {
return result('test', 'Pull requests always run the full Lua suite.');
}
const repo = context.repo;
const isTag = context.ref.startsWith('refs/tags/');
try {
let head;
let pr;
if (isTag) {
head = commit;
} else if (context.ref === 'refs/heads/master') {
const pulls = await github.paginate(github.rest.repos.listPullRequestsAssociatedWithCommit,
{ ...repo, commit_sha: commit, per_page: 100 });
pr = pulls.find(p => p.merged_at && p.base.ref === 'master' &&
p.base.repo.full_name === `${repo.owner}/${repo.repo}` && p.merge_commit_sha === commit);
if (!pr) return result('test', 'No merged PR found for this master commit; running the full suite.');
head = pr.head.sha;
} else {
return result('test', 'No reusable validation source for this event; running the full suite.');
}

const runs = await github.paginate(github.rest.actions.listWorkflowRuns, {
...repo, workflow_id: 'Test.yml', head_sha: head,
event: isTag ? 'push' : 'pull_request', per_page: 100,
});
// Do not select only successful runs: that could hide a newer failure.
let run = runs.filter(r => r.id !== context.runId &&
(isTag ? r.head_branch === 'master' :
// GitHub can omit pull_requests for fork runs. The head SHA and
// matching tested tree still identify the source in that case.
!r.pull_requests?.length || r.pull_requests.some(p => p.number === pr.number)))
.sort((a, b) => b.id - a.id)[0];
if (!run) return result('test', 'No matching CI run found; running the full suite.');

const deadline = now() + waitMs;
while (run.status !== 'completed') {
if (now() >= deadline) {
return result('test', `CI run ${run.html_url} did not finish within 70 minutes; running the full suite.`);
}
core.info(`Waiting for ${run.html_url} (${run.status}).`);
await sleep(30_000);
({ data: run } = await github.rest.actions.getWorkflowRun({ ...repo, run_id: run.id }));
}

const failed = ['failure', 'timed_out', 'action_required'].includes(run.conclusion);
if (isTag && failed) {
return result('block', `Validation failed for this commit: ${run.html_url}. Fix or rerun that CI run before proceeding.`);
}
const artifacts = await github.paginate(github.rest.actions.listWorkflowRunArtifacts,
{ ...repo, run_id: run.id, per_page: 100 });
const proof = artifacts.some(a => !a.expired &&
a.name === `lua-validation-${tree}-attempt-${run.run_attempt}`);
// A master run is already tied to this exact commit. A PR run needs a
// matching tree artifact because head_sha identifies its branch head,
// whereas checkout tests GitHub's temporary merge commit.
if (proof && failed) {
return result('block', `Validation failed for this source: ${run.html_url}. Fix or rerun that CI run before proceeding.`);
}
if (run.conclusion === 'success' && proof) {
const source = isTag ? `master CI for tagged commit ${commit}` : `PR #${pr.number} CI`;
return result('reuse', `${source} passed, and its recorded Git tree exactly matches ${tree}. Reusing run ${run.html_url} (attempt ${run.run_attempt}).`);
}
if (!proof) {
return result('test', `CI run ${run.html_url} (attempt ${run.run_attempt}) has no unexpired validation artifact matching Git tree ${tree}; running the full suite.`);
}
return result('test', `CI run ${run.html_url} concluded ${run.conclusion}; running the full suite.`);
} catch (error) {
core.warning(`Could not verify prior validation: ${error.message}`);
return result('test', `Prior validation could not be verified (${error.message}); running the full suite.`);
}
};
Loading
Loading