mirror of
https://github.com/rustfs/rustfs.git
synced 2026-11-02 17:13:37 +00:00
Compare commits
2 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 88587a8f94 | |||
| 8c8b5da63f |
@@ -346,12 +346,12 @@ jobs:
|
||||
path: ${{ steps.publish.outputs.candidate_file }}
|
||||
if-no-files-found: error
|
||||
|
||||
# Publish the deb/rpm pair to the auto-testing repo's `assets` branch so
|
||||
# engineers can download and install the nightly directly. The branch is
|
||||
# a single-commit orphan rewritten on every build, which keeps the repo
|
||||
# small while the latest files stay reachable at stable raw URLs.
|
||||
# Publish the deb/rpm pair as assets of the rolling `nightly` release on
|
||||
# rustfs/auto-testing (see scripts/release/publish_nightly_assets.sh).
|
||||
# The dispatch below runs only after this upload and the immutable R2
|
||||
# candidate both succeed. It does not replace rustfs-functional-chain.yml,
|
||||
# which still starts from this workflow's workflow_run completion.
|
||||
# kms-vault-lane and kms-vault-ha-failover stay parallel with this job.
|
||||
- name: Publish packages to auto-testing release assets
|
||||
env:
|
||||
ASSETS_TOKEN: ${{ secrets.PF_TESTING_GH_TOKEN }}
|
||||
@@ -360,6 +360,90 @@ jobs:
|
||||
DEB_DATE: ${{ steps.deb.outputs.deb_date }}
|
||||
BUILD_REF: ${{ env.NIGHTLY_BUILD_REF }}
|
||||
run: bash scripts/release/publish_nightly_assets.sh
|
||||
|
||||
# Cross-repo repository_dispatch cannot use GITHUB_TOKEN. The secret is a
|
||||
# PAT or GitHub App token for rustfs/auto-testing: classic `repo` scope,
|
||||
# or fine-grained Contents write (the permission GitHub checks for
|
||||
# POST /repos/{owner}/{repo}/dispatches). A dispatch failure fails this
|
||||
# step, and therefore this job, so the scheduled failure alert fires.
|
||||
# GitHub allows at most 10 top-level client_payload properties, so the
|
||||
# schema 2 candidate object is nested under `candidate` and the release
|
||||
# fields sit beside it.
|
||||
- name: Dispatch nightly candidate to auto-testing
|
||||
if: ${{ steps.publish.outputs.candidate_file != '' }}
|
||||
env:
|
||||
DISPATCH_TOKEN: ${{ secrets.AUTO_TESTING_DISPATCH_TOKEN }}
|
||||
CANDIDATE_FILE: ${{ steps.publish.outputs.candidate_file }}
|
||||
DEB_DATE: ${{ steps.deb.outputs.deb_date }}
|
||||
SOURCE_REPO: ${{ github.repository }}
|
||||
shell: bash
|
||||
run: |
|
||||
set -euo pipefail
|
||||
: "${DISPATCH_TOKEN:?AUTO_TESTING_DISPATCH_TOKEN is required to dispatch rustfs/auto-testing}"
|
||||
: "${CANDIDATE_FILE:?nightly candidate manifest is required}"
|
||||
: "${DEB_DATE:?DEB_DATE is required}"
|
||||
[[ "${DEB_DATE}" =~ ^[0-9]{4}-[0-9]{2}-[0-9]{2}$ ]]
|
||||
[[ "${SOURCE_REPO}" == "rustfs/rustfs" ]]
|
||||
|
||||
payload="${RUNNER_TEMP}/nightly-candidate-dispatch-${GITHUB_RUN_ID}-${GITHUB_RUN_ATTEMPT}.json"
|
||||
jq -n --slurpfile candidate "${CANDIDATE_FILE}" \
|
||||
--arg event_type "nightly-candidate-ready" \
|
||||
--arg source_repo "${SOURCE_REPO}" \
|
||||
--arg latest_alias_url "https://dl.rustfs.com/artifacts/rustfs/packages/nightly/rustfs-nightly-latest.deb" \
|
||||
--arg auto_testing_release "https://github.com/rustfs/auto-testing/releases/tag/nightly" \
|
||||
--arg channel "nightly" \
|
||||
--arg deb_date "${DEB_DATE}" \
|
||||
--arg nightly_workflow ".github/workflows/nightly-gnu.yml" \
|
||||
'{
|
||||
event_type: $event_type,
|
||||
source_repo: $source_repo,
|
||||
latest_alias_url: $latest_alias_url,
|
||||
auto_testing_release: $auto_testing_release,
|
||||
channel: $channel,
|
||||
deb_date: $deb_date,
|
||||
nightly_workflow: $nightly_workflow,
|
||||
candidate: $candidate[0]
|
||||
}' > "${payload}"
|
||||
if ! jq -e '
|
||||
.event_type == "nightly-candidate-ready"
|
||||
and .source_repo == "rustfs/rustfs"
|
||||
and .channel == "nightly"
|
||||
and .deb_date == env.DEB_DATE
|
||||
and .nightly_workflow == ".github/workflows/nightly-gnu.yml"
|
||||
and .latest_alias_url == "https://dl.rustfs.com/artifacts/rustfs/packages/nightly/rustfs-nightly-latest.deb"
|
||||
and .auto_testing_release == "https://github.com/rustfs/auto-testing/releases/tag/nightly"
|
||||
and .candidate.schema == 2
|
||||
and ((.candidate | keys | sort) == ["build_run_attempt","build_run_id","package_sha256","package_url","schema","source_ref","source_sha","workflow_sha"])
|
||||
and ((keys | sort) == ["auto_testing_release","candidate","channel","deb_date","event_type","latest_alias_url","nightly_workflow","source_repo"])
|
||||
' "${payload}" >/dev/null; then
|
||||
echo "nightly dispatch payload does not match the nightly-candidate-ready contract" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
body="${RUNNER_TEMP}/nightly-candidate-dispatch-body-${GITHUB_RUN_ID}-${GITHUB_RUN_ATTEMPT}.json"
|
||||
jq -n --slurpfile client_payload "${payload}" \
|
||||
'{event_type: "nightly-candidate-ready", client_payload: $client_payload[0]}' > "${body}"
|
||||
response="${RUNNER_TEMP}/nightly-candidate-dispatch-response-${GITHUB_RUN_ID}-${GITHUB_RUN_ATTEMPT}.txt"
|
||||
status="$(curl -sS --retry 3 --connect-timeout 15 --max-time 60 \
|
||||
-o "${response}" -w '%{http_code}' \
|
||||
-X POST \
|
||||
-H "Authorization: Bearer ${DISPATCH_TOKEN}" \
|
||||
-H "Accept: application/vnd.github+json" \
|
||||
-H "X-GitHub-Api-Version: 2022-11-28" \
|
||||
-H "Content-Type: application/json" \
|
||||
--data-binary @"${body}" \
|
||||
https://api.github.com/repos/rustfs/auto-testing/dispatches)" || {
|
||||
echo "repository_dispatch request to rustfs/auto-testing failed before an HTTP status" >&2
|
||||
exit 1
|
||||
}
|
||||
if [[ "${status}" != "204" ]]; then
|
||||
echo "repository_dispatch to rustfs/auto-testing failed: HTTP ${status}" >&2
|
||||
if [[ -s "${response}" ]]; then
|
||||
cat "${response}" >&2
|
||||
fi
|
||||
exit 1
|
||||
fi
|
||||
echo "dispatched nightly-candidate-ready to rustfs/auto-testing (deb_date=${DEB_DATE}, run=${GITHUB_RUN_ID}, attempt=${GITHUB_RUN_ATTEMPT})"
|
||||
# Live-Vault lane for the rustfs-kms suite (rustfs/backlog#1774).
|
||||
#
|
||||
# RUSTFS_KMS_VAULT_TOKEN is the single switch that adds the Vault KV2 and
|
||||
|
||||
@@ -640,12 +640,12 @@ fn http_range_spec_from_get_range(range: &GetRange) -> HTTPRangeSpec {
|
||||
GetRange::Bounded(range) => http_range_spec_from_range(range.clone()),
|
||||
GetRange::Offset(start) => HTTPRangeSpec {
|
||||
is_suffix_length: false,
|
||||
start: *start as i64,
|
||||
start: i64::try_from(*start).unwrap_or(i64::MAX),
|
||||
end: -1,
|
||||
},
|
||||
GetRange::Suffix(length) => HTTPRangeSpec {
|
||||
is_suffix_length: true,
|
||||
start: *length as i64,
|
||||
start: i64::try_from(*length).unwrap_or(i64::MAX),
|
||||
end: -1,
|
||||
},
|
||||
}
|
||||
@@ -654,15 +654,15 @@ fn http_range_spec_from_get_range(range: &GetRange) -> HTTPRangeSpec {
|
||||
fn http_range_spec_from_range(range: Range<u64>) -> HTTPRangeSpec {
|
||||
HTTPRangeSpec {
|
||||
is_suffix_length: false,
|
||||
start: range.start as i64,
|
||||
end: range.end.saturating_sub(1) as i64,
|
||||
start: i64::try_from(range.start).unwrap_or(i64::MAX),
|
||||
end: i64::try_from(range.end.saturating_sub(1)).unwrap_or(i64::MAX),
|
||||
}
|
||||
}
|
||||
|
||||
fn http_range_spec_from_start(start: u64) -> HTTPRangeSpec {
|
||||
HTTPRangeSpec {
|
||||
is_suffix_length: false,
|
||||
start: start as i64,
|
||||
start: i64::try_from(start).unwrap_or(i64::MAX),
|
||||
end: -1,
|
||||
}
|
||||
}
|
||||
@@ -2476,6 +2476,48 @@ mod test {
|
||||
assert_eq!(range.end, 19);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_get_range_conversion_for_suffix_ranges() {
|
||||
// Normal suffix: last 100 bytes.
|
||||
let range = http_range_spec_from_get_range(&GetRange::Suffix(100));
|
||||
assert!(range.is_suffix_length);
|
||||
assert_eq!(range.start, 100);
|
||||
assert_eq!(range.end, -1);
|
||||
|
||||
// Overflowing suffix: `u64::MAX` previously wrapped to `-1` via `as i64`
|
||||
// and was misread as "last 1 byte". It now clamps to `i64::MAX`, which
|
||||
// `get_length` treats as "the whole object".
|
||||
let range = http_range_spec_from_get_range(&GetRange::Suffix(u64::MAX));
|
||||
assert!(range.is_suffix_length);
|
||||
assert_eq!(range.start, i64::MAX);
|
||||
assert_eq!(range.end, -1);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_get_range_conversion_for_offset_overflow() {
|
||||
// Normal offset.
|
||||
let range = http_range_spec_from_get_range(&GetRange::Offset(42));
|
||||
assert!(!range.is_suffix_length);
|
||||
assert_eq!(range.start, 42);
|
||||
assert_eq!(range.end, -1);
|
||||
|
||||
// Overflowing offset clamps to `i64::MAX`, which is then rejected as
|
||||
// out-of-range (416) by `get_offset_length`.
|
||||
let range = http_range_spec_from_get_range(&GetRange::Offset(u64::MAX));
|
||||
assert!(!range.is_suffix_length);
|
||||
assert_eq!(range.start, i64::MAX);
|
||||
assert_eq!(range.end, -1);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_get_range_conversion_for_bounded_overflow() {
|
||||
// Overflowing bounded range clamps start/end to `i64::MAX`.
|
||||
let range = http_range_spec_from_get_range(&GetRange::Bounded(u64::MAX - 10..u64::MAX));
|
||||
assert!(!range.is_suffix_length);
|
||||
assert_eq!(range.start, i64::MAX);
|
||||
assert_eq!(range.end, i64::MAX);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_scan_range_output_can_convert_field_delimiter() {
|
||||
let chunks = stream::iter(vec![Ok::<_, std::io::Error>(Bytes::from_static(b"a&&1\nb&&2\n"))]);
|
||||
|
||||
@@ -220,6 +220,126 @@ SH
|
||||
self.assertNotIn(" continue-on-error: true", self.publish)
|
||||
self.assertNotIn(" overwrite: true", upload)
|
||||
|
||||
def dispatch_shell(self):
|
||||
step = self.steps["Dispatch nightly candidate to auto-testing"]
|
||||
start = step.index(" run: |") + 1
|
||||
return "\n".join(line[10:] for line in step[start:] if not line.strip() or line.startswith(" "))
|
||||
|
||||
def candidate_manifest(self, **overrides):
|
||||
digest = overrides.pop("package_sha256", self.digest)
|
||||
workflow_sha = overrides.pop("workflow_sha", self.sha)
|
||||
manifest = {
|
||||
"schema": 2,
|
||||
"workflow_sha": workflow_sha,
|
||||
"source_ref": "main",
|
||||
"source_sha": self.sha,
|
||||
"build_run_id": 12345,
|
||||
"build_run_attempt": 1,
|
||||
"package_sha256": digest,
|
||||
"package_url": f"https://dl.rustfs.com/artifacts/rustfs/packages/nightly/runs/12345/1/{digest}/rustfs.deb",
|
||||
}
|
||||
manifest.update(overrides)
|
||||
return manifest
|
||||
|
||||
def run_dispatch(self, manifest=None, **overrides):
|
||||
manifest = self.candidate_manifest() if manifest is None else manifest
|
||||
candidate = self.root / "candidate.json"
|
||||
candidate.write_text(json.dumps(manifest))
|
||||
shim = self.root / "dispatch-curl.sh"
|
||||
shim.write_text(r'''curl() {
|
||||
printf '%s\0' "$@" > "$FAKE_CURL_ARGS"
|
||||
local outfile="" data=""
|
||||
local args=("$@")
|
||||
local i=0
|
||||
while [[ $i -lt ${#args[@]} ]]; do
|
||||
case "${args[$i]}" in
|
||||
-o) outfile="${args[$((i+1))]}"; i=$((i+2));;
|
||||
--data-binary)
|
||||
data="${args[$((i+1))]}"
|
||||
cp "${data#@}" "$FAKE_DISPATCH_BODY"
|
||||
i=$((i+2));;
|
||||
*) i=$((i+1));;
|
||||
esac
|
||||
done
|
||||
[[ "$FAKE_DISPATCH_STATUS" != "curl-failure" ]] || return 28
|
||||
if [[ -n "$outfile" ]]; then printf '%s' "$FAKE_DISPATCH_RESPONSE" > "$outfile"; fi
|
||||
printf '%s' "$FAKE_DISPATCH_STATUS"
|
||||
}
|
||||
''')
|
||||
env = dict(os.environ, BASH_ENV=str(shim), CANDIDATE_FILE=str(candidate), DEB_DATE="2026-09-06",
|
||||
SOURCE_REPO="rustfs/rustfs", DISPATCH_TOKEN="test-dispatch-token", RUNNER_TEMP=str(self.root),
|
||||
GITHUB_RUN_ID="12345", GITHUB_RUN_ATTEMPT="1", GITHUB_SHA=manifest["workflow_sha"],
|
||||
FAKE_CURL_ARGS=str(self.root / "curl-args"), FAKE_DISPATCH_BODY=str(self.root / "dispatch-body.json"),
|
||||
FAKE_DISPATCH_STATUS="204", FAKE_DISPATCH_RESPONSE="")
|
||||
env.update(overrides)
|
||||
return subprocess.run(["bash", "--noprofile", "--norc", "-e", "-o", "pipefail", "-c", self.dispatch_shell()],
|
||||
cwd=self.root, env=env, capture_output=True, text=True)
|
||||
|
||||
def test_dispatch_follows_release_publish_without_blocking_vault_lanes(self):
|
||||
names = list(self.steps)
|
||||
self.assertLess(names.index("Publish packages to auto-testing release assets"), names.index("Dispatch nightly candidate to auto-testing"))
|
||||
step = "\n".join(self.steps["Dispatch nightly candidate to auto-testing"])
|
||||
self.assertIn(" if: ${{ steps.publish.outputs.candidate_file != '' }}", step)
|
||||
self.assertIn(" DISPATCH_TOKEN: ${{ secrets.AUTO_TESTING_DISPATCH_TOKEN }}", step)
|
||||
self.assertNotIn("secrets.GITHUB_TOKEN", step)
|
||||
self.assertNotIn("github.token", step)
|
||||
self.assertNotIn("continue-on-error", step)
|
||||
self.assertIn("https://api.github.com/repos/rustfs/auto-testing/dispatches", step)
|
||||
self.assertIn("nightly-candidate-ready", step)
|
||||
lines = WORKFLOW.read_text().splitlines()
|
||||
for lane in ("kms-vault-lane", "kms-vault-ha-failover"):
|
||||
job = yaml_block(lines, lane, 2)
|
||||
needs = next(line for line in job if line.strip().startswith("needs:"))
|
||||
self.assertEqual(needs.strip(), "needs: resolve-source")
|
||||
self.assertNotIn("Dispatch nightly candidate", "\n".join(job))
|
||||
alert = "\n".join(yaml_block(lines, "alert-on-failure", 2))
|
||||
self.assertIn("needs: [build, kms-vault-lane, kms-vault-ha-failover]", alert)
|
||||
chain = (ROOT / ".github/workflows/rustfs-functional-chain.yml").read_text()
|
||||
self.assertIn('workflows: ["Nightly GNU Build"]', chain)
|
||||
self.assertIn("types: [completed]", chain)
|
||||
|
||||
def test_dispatch_sends_schema2_candidate_without_the_token(self):
|
||||
result = self.run_dispatch()
|
||||
self.assertEqual(result.returncode, 0, result.stderr)
|
||||
body = json.loads((self.root / "dispatch-body.json").read_text())
|
||||
self.assertEqual(body["event_type"], "nightly-candidate-ready")
|
||||
payload = body["client_payload"]
|
||||
self.assertLessEqual(len(payload), 10)
|
||||
self.assertEqual(set(payload), {"event_type", "source_repo", "latest_alias_url", "auto_testing_release", "channel", "deb_date", "nightly_workflow", "candidate"})
|
||||
self.assertEqual(payload["candidate"], self.candidate_manifest())
|
||||
self.assertEqual(payload["latest_alias_url"], "https://dl.rustfs.com/artifacts/rustfs/packages/nightly/rustfs-nightly-latest.deb")
|
||||
self.assertEqual(payload["auto_testing_release"], "https://github.com/rustfs/auto-testing/releases/tag/nightly")
|
||||
self.assertEqual(payload["channel"], "nightly")
|
||||
self.assertEqual(payload["deb_date"], "2026-09-06")
|
||||
self.assertEqual(payload["nightly_workflow"], ".github/workflows/nightly-gnu.yml")
|
||||
self.assertEqual(payload["source_repo"], "rustfs/rustfs")
|
||||
validate_manifest(payload["candidate"], {"id": 12345, "run_attempt": 1, "head_sha": self.sha})
|
||||
args = (self.root / "curl-args").read_bytes().split(b"\0")
|
||||
self.assertIn(b"https://api.github.com/repos/rustfs/auto-testing/dispatches", args)
|
||||
self.assertIn(b"Authorization: Bearer test-dispatch-token", args)
|
||||
self.assertNotIn("test-dispatch-token", json.dumps(body))
|
||||
self.assertIn("dispatched nightly-candidate-ready", result.stdout)
|
||||
|
||||
def test_dispatch_failure_stops_before_treating_the_candidate_as_notified(self):
|
||||
for status, response in (("422", '{"message":"No more than 10 properties are allowed"}'), ("500", "unavailable"), ("curl-failure", "")):
|
||||
with self.subTest(status=status):
|
||||
result = self.run_dispatch(FAKE_DISPATCH_STATUS=status, FAKE_DISPATCH_RESPONSE=response)
|
||||
self.assertNotEqual(result.returncode, 0, result.stdout)
|
||||
self.assertIn("repository_dispatch", result.stderr)
|
||||
if response:
|
||||
self.assertIn(response, result.stderr)
|
||||
self.assertNotIn("dispatched nightly-candidate-ready", result.stdout)
|
||||
for marker in ("curl-args", "dispatch-body.json"):
|
||||
(self.root / marker).unlink(missing_ok=True)
|
||||
missing = self.run_dispatch(DISPATCH_TOKEN="")
|
||||
self.assertNotEqual(missing.returncode, 0)
|
||||
self.assertIn("AUTO_TESTING_DISPATCH_TOKEN is required", missing.stderr)
|
||||
self.assertFalse((self.root / "curl-args").exists())
|
||||
rejected = self.run_dispatch(manifest=self.candidate_manifest(extra="nope"))
|
||||
self.assertNotEqual(rejected.returncode, 0, rejected.stdout)
|
||||
self.assertIn("nightly-candidate-ready contract", rejected.stderr)
|
||||
self.assertFalse((self.root / "dispatch-body.json").exists())
|
||||
|
||||
|
||||
class ValidationPackageTests(unittest.TestCase):
|
||||
def setUp(self):
|
||||
|
||||
Reference in New Issue
Block a user