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
20 changes: 17 additions & 3 deletions .github/scripts/bootstrap-common-workflows.sh
Original file line number Diff line number Diff line change
Expand Up @@ -75,19 +75,20 @@ declare -A BOOTSTRAPPED_FILES
# secrets: inherit
BOOTSTRAPPED_FILES[".github/workflows/cleanup-pr-artifacts.yml"]="bmFtZTogQ2xlYW51cCBQUiBBcnRpZmFjdHMKCm9uOgogIHB1bGxfcmVxdWVzdDoKICAgIHR5cGVzOiBbY2xvc2VkXQoKam9iczoKICBjbGVhbnVwOgogICAgdXNlczogQ3JhdGlzL1dvcmtmbG93cy8uZ2l0aHViL3dvcmtmbG93cy9jbGVhbnVwLXByLWFydGlmYWN0cy55bWxAbWFpbgogICAgd2l0aDoKICAgICAgcHVsbF9yZXF1ZXN0OiAke3sgZ2l0aHViLmV2ZW50LnB1bGxfcmVxdWVzdC5udW1iZXIgfX0KICAgIHNlY3JldHM6IGluaGVyaXQK"

# update-packages.yml — nightly scheduled + manual trigger, delegates to reusable workflow
# update-packages.yml — weekly scheduled + manual trigger, delegates to reusable workflow
# The cron placeholder is replaced per repository before creating its blob.
# Decodes to:
# name: Update Packages
# on:
# schedule:
# - cron: '0 6 * * *'
# - cron: '__PACKAGE_UPDATE_CRON__'
# workflow_dispatch:
# jobs:
# update:
# uses: Cratis/Workflows/.github/workflows/update-packages.yml@main
# secrets:
# PAT_WORKFLOWS: ${{ secrets.PAT_WORKFLOWS }}
BOOTSTRAPPED_FILES[".github/workflows/update-packages.yml"]="bmFtZTogVXBkYXRlIFBhY2thZ2VzCgpvbjoKICBzY2hlZHVsZToKICAgIC0gY3JvbjogJzAgNiAqICogKicKICB3b3JrZmxvd19kaXNwYXRjaDoKCmpvYnM6CiAgdXBkYXRlOgogICAgdXNlczogQ3JhdGlzL1dvcmtmbG93cy8uZ2l0aHViL3dvcmtmbG93cy91cGRhdGUtcGFja2FnZXMueW1sQG1haW4KICAgIHNlY3JldHM6CiAgICAgIFBBVF9XT1JLRkxPV1M6ICR7eyBzZWNyZXRzLlBBVF9XT1JLRkxPV1MgfX0K"
BOOTSTRAPPED_FILES[".github/workflows/update-packages.yml"]="bmFtZTogVXBkYXRlIFBhY2thZ2VzCgpvbjoKICBzY2hlZHVsZToKICAgIC0gY3JvbjogJ19fUEFDS0FHRV9VUERBVEVfQ1JPTl9fJwogIHdvcmtmbG93X2Rpc3BhdGNoOgoKam9iczoKICB1cGRhdGU6CiAgICB1c2VzOiBDcmF0aXMvV29ya2Zsb3dzLy5naXRodWIvd29ya2Zsb3dzL3VwZGF0ZS1wYWNrYWdlcy55bWxAbWFpbgogICAgc2VjcmV0czoKICAgICAgUEFUX1dPUktGTE9XUzogJHt7IHNlY3JldHMuUEFUX1dPUktGTE9XUyB9fQo="

# auto-approve-publish-deployments.yml — approves pending npm/nuget deployments
# for Publish workflow runs (trusted publishing environments).
Expand Down Expand Up @@ -230,6 +231,14 @@ BOOTSTRAPPED_FILES[".github/workflows/verify-semver-label.yml"]="bmFtZTogVmVyaWZ
# id: ca1031
BOOTSTRAPPED_FILES[".github/codeql/codeql-config.yml"]="bmFtZTogIkNyYXRpcyBDb2RlUUwgY29uZmlnIgoKcXVlcnktZmlsdGVyczoKICAjIENBMTAzMSBpcyBpbnRlbnRpb25hbGx5IGV4Y2x1ZGVkIGZyb20gdGhlIHNoYXJlZCBiYXNlbGluZS4KICAtIGV4Y2x1ZGU6CiAgICAgIGlkOiBjYTEwMzEK"

# Stable POSIX checksum of the repository name: Monday, minute 1-59, 03:00-07:59 UTC.
# Unlike an array index, this does not move existing schedules when repositories are added.
package_update_cron() {
local checksum remainder
read -r checksum remainder < <(printf '%s' "$1" | cksum)
printf '%d %d * * 1' "$((checksum % 59 + 1))" "$((checksum / 59 % 5 + 3))"
}

# ================================================================
# Per-file skips
# ================================================================
Expand Down Expand Up @@ -395,6 +404,11 @@ echo "$repos" | jq -r '.[]' | while read -r repo; do
fi
fi
file_b64="${BOOTSTRAPPED_FILES[$file_path]}"
if [ "$file_path" = ".github/workflows/update-packages.yml" ]; then
file_content=$(printf '%s' "$file_b64" | base64 -d)
file_content="${file_content/__PACKAGE_UPDATE_CRON__/$(package_update_cron "$repo")}"
file_b64=$(printf '%s\n' "$file_content" | base64 | tr -d '\n')
fi
case "$file_path" in
.github/codeql/codeql-config.yml)
file_label="codeql-config"
Expand Down
64 changes: 64 additions & 0 deletions .github/scripts/tests/bootstrap-schedules.test.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
# Copyright (c) Cratis. All rights reserved.
# Licensed under the MIT license. See LICENSE file in the project root for full license information.
"""Offline checks of the bootstrap's repository-specific package-update wrapper."""
import base64
from pathlib import Path
import re
import subprocess
import unittest

ROOT = Path(__file__).resolve().parents[3]
SOURCE = (ROOT / ".github/scripts/bootstrap-common-workflows.sh").read_text()
TEMPLATE = re.search(
r'BOOTSTRAPPED_FILES\[".github/workflows/update-packages.yml"\]="([^"]+)"', SOURCE)[1]
CRON_FUNCTION = re.search(r"^package_update_cron\(\) \{.*?^\}", SOURCE, re.M | re.S)[0]
RENDER = re.search(
r'^ if \[ "\$file_path" = "\.github/workflows/update-packages.yml" \]; then\n.*?^ fi',
SOURCE, re.M | re.S)[0]


def render(repo):
# Execute the production rendering block without invoking the live bootstrap,
# its PAT probe, or any GitHub writes.
return subprocess.check_output([
"bash", "-c", 'set -euo pipefail\n' + CRON_FUNCTION + '\n'
'repo="$1"\nfile_b64="$2"\nfile_path=".github/workflows/update-packages.yml"\n'
+ RENDER + '\nprintf "%s" "$file_b64"', "bootstrap-test", repo, TEMPLATE,
], text=True)


class BootstrapSchedulesTests(unittest.TestCase):
def test_only_the_schedule_changes(self):
template = base64.b64decode(TEMPLATE).decode()
rendered = base64.b64decode(render("Arc")).decode()
cron = re.search(r"cron: '([^']+)'", rendered)[1]
self.assertEqual(rendered, template.replace("__PACKAGE_UPDATE_CRON__", cron))
self.assertIn(" workflow_dispatch:", rendered)
self.assertNotIn("__PACKAGE_UPDATE_CRON__", rendered)

def test_weekly_offsets_are_stable_and_off_the_hour(self):
crons = set()
for repo in ["Arc", "Chronicle", "Fundamentals", "cli", "Arc.TypeScript", "Cratis-Example"]:
first = render(repo)
self.assertEqual(first, render(repo))
cron = re.search(r"cron: '([^']+)'", base64.b64decode(first).decode())[1]
minute, hour, day, month, weekday = cron.split()
self.assertTrue(1 <= int(minute) <= 59)
self.assertTrue(3 <= int(hour) <= 7)
self.assertEqual((day, month, weekday), ("*", "*", "1"))
crons.add(cron)
self.assertGreater(len(crons), 1)

def test_private_callers_use_the_same_deterministic_offsets(self):
expected = {
"Direct": "37 5 * * 1", "Studio": "48 5 * * 1",
"Infrastructure": "5 3 * * 1", "Ensemble": "32 4 * * 1",
"Experiments": "57 5 * * 1",
}
for repo, cron in expected.items():
with self.subTest(repo=repo):
self.assertIn("cron: '" + cron + "'", base64.b64decode(render(repo)).decode())


if __name__ == "__main__":
unittest.main(verbosity=2)
Loading