Skip to content
Open
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
23 changes: 23 additions & 0 deletions .github.jsonnet
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
local util = import '.github/jsonnet/index.jsonnet';
local image = 'mirror.gcr.io/node:24';

util.workflowJavascriptPackage(
repositories=['github', 'gynzy'],
packageManager='pnpm',
branch='main',
isPublicFork=true,
buildSteps=[],
testJob=util.ghJob(
'test',
image=image,
useCredentials=false,
runsOn='ubuntu-latest',
steps=[
util.pnpm.checkoutAndPnpm(
ref='${{ github.event.pull_request.head.sha }}',
source='github',
),
util.step('lint', 'pnpm run lint'),
],
),
)
1 change: 1 addition & 0 deletions .github/jsonnet/GIT_VERSION
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
74f2c2b8369058df7595c9126698868b474fd90c
2 changes: 2 additions & 0 deletions .github/jsonnet/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
These files come from https://www.github.com/gynzy/lib-jsonnet/
Do not update here, but extend the libraries upstream.
16 changes: 16 additions & 0 deletions .github/jsonnet/actions.jsonnet
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
/**
* GitHub Action plugin references
*
* Centralised SHA-pinned references for external GitHub Actions used across workflows.
* Pinning to a SHA (rather than a tag) protects against supply-chain attacks where a
* tag is moved to point at a malicious commit. The trailing comment records the
* human-readable version that the SHA corresponds to at the time of pinning.
*/
{
checkout_action: 'actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd', // v6
gcp_auth_action: 'google-github-actions/auth@7c6bc770dae815cd3e89ee6cdf493a5fab2cc093', // v3
gcp_setup_gcloud_action: 'google-github-actions/setup-gcloud@aa5489c8933f4cc7a4f7d45035b3b1440c9c10db', // v3
pulumi_action: 'pulumi/actions@cd99a7f8865434dd3532b586a26f9ebea596894f', // v5
onepassword_load_secrets_action: '1password/load-secrets-action@92467eb28f72e8255933372f1e0707c567ce2259', // v4
slack_action: 'act10ns/slack@d96404edccc6d6467fc7f8134a420c851b1e9054', // v2
}
186 changes: 186 additions & 0 deletions .github/jsonnet/base.jsonnet
Original file line number Diff line number Diff line change
@@ -0,0 +1,186 @@
local images = import 'images.jsonnet';
local misc = import 'misc.jsonnet';

{
/**
* Creates a complete GitHub Actions workflow pipeline with multiple jobs.
*
* @param {string} name - The name of the workflow (becomes the .yml filename)
* @param {array of jobs} jobs - Array of job objects (created with ghJob, ghExternalJob, etc.)
* @param {array} [event=['pull_request']] - GitHub events that trigger this workflow
* @param {object} [permissions=null] - Permissions for the workflow (e.g., {contents: 'read'})
* @param {object} [concurrency=null] - Concurrency settings to limit parallel runs
* @returns {workflows} - GitHub Actions YAML manifest
*/
pipeline(name, jobs, event=['pull_request'], permissions=null, concurrency=null):: {
[name + '.yml']:
'# GENERATED with jsonnet - DO NOT EDIT MANUALLY\n' +
std.manifestYamlDoc(
{
name: name,
on: event,
jobs: std.foldl(function(x, y) x + y, jobs, {}),
} + (if permissions == null then {} else { permissions: permissions }) + (if concurrency == null then {} else { concurrency: concurrency }),
),
},

/**
* Creates a GitHub Actions job that runs on a containerized runner.
*
* @param {string} name - The name of the job (used as the job key)
* @param {number} [timeoutMinutes=30] - Maximum time in minutes before job is cancelled. Max value is 55, after which the runner is killed.
* @param {string} [runsOn=null] - Runner type (defaults to 'arc-runner-2')
* @param {string} [image=images.default_job_image] - Docker image to run the job in
* @param {steps} [steps=[]] - Array of step objects (created with step() or action())
* @param {string} [ifClause=null] - Conditional expression to determine if job should run
* @param {array} [needs=null] - Array of job names this job depends on
* @param {object} [outputs=null] - Job outputs available to dependent jobs
* @param {boolean} [useCredentials=true] - Whether to use Docker registry credentials. Must be set to false for public images.
* @param {object} [services=null] - Service containers to run alongside the job
* @param {object} [permissions=null] - Job-level permissions (overrides workflow permissions)
* @param {object} [concurrency=null] - Job-level concurrency settings
* @param {boolean} [continueOnError=null] - Whether to continue workflow if job fails
* @param {object} [env=null] - Environment variables for all steps in the job
* @param {object} [strategy=null] - GitHub Actions matrix strategy (e.g., {matrix: {shard: [1,2,3]}, 'fail-fast': false})
* @returns {jobs} - GitHub Actions job definition
*/
ghJob(
name,
timeoutMinutes=30,
runsOn=null,
image=images.default_job_image,
steps=[],
ifClause=null,
needs=null,
outputs=null,
useCredentials=true,
services=null,
permissions=null,
concurrency=null,
continueOnError=null,
env=null,
strategy=null,
)::
{
[name]: {
'timeout-minutes': timeoutMinutes,
'runs-on': (if runsOn == null then 'arc-runner-2' else runsOn),
} +
(
if image == null then {} else
{
container: {
image: image,
} + (if useCredentials then { credentials: { username: '_json_key', password: misc.secret('docker_gcr_io') } } else {}),
}
) +
{
steps: std.flattenArrays(steps),
} +
(if ifClause != null then { 'if': ifClause } else {}) +
(if needs != null then { needs: needs } else {}) +
(if outputs != null then { outputs: outputs } else {}) +
(if services != null then { services: services } else {}) +
(if permissions == null then {} else { permissions: permissions }) +
(if concurrency == null then {} else { concurrency: concurrency }) +
(if continueOnError == null then {} else { 'continue-on-error': continueOnError }) +
(if env == null then {} else { env: env }) +
(if strategy == null then {} else { strategy: strategy }),
},

/**
* Creates a GitHub Actions job that uses a reusable workflow from another repository.
*
* @param {string} name - The name of the job (used as the job key)
* @param {string} uses - The reusable workflow reference (e.g., 'owner/repo/.github/workflows/workflow.yml@ref')
* @param {object} [with=null] - Input parameters to pass to the reusable workflow
* @returns {jobs} - GitHub Actions external job definition
*/
ghExternalJob(
name,
uses,
with=null,
)::
{
[name]: {
uses: uses,
} + (if with != null then {
with: with,
} else {}),
},

/**
* Creates a GitHub Actions step that runs shell commands.
*
* @docs https://docs.github.com/en/actions/reference/workflows-and-actions/workflow-syntax#jobsjob_idsteps
*
* @param {string} name - Display name for the step in the GitHub UI
* @param {string} run - Shell command(s) to execute
* @param {object} [env=null] - Environment variables for this step
* @param {string} [workingDirectory=null] - Directory to run the command in
* @param {string} [ifClause=null] - Conditional expression to determine if step should run
* @param {string} [id=null] - Unique identifier for this step (used to reference outputs)
* @param {boolean} [continueOnError=null] - Whether to continue job if this step fails, defaults to false
* @param {string} [shell=null] - Shell to use for running commands (e.g., 'bash', 'python', 'powershell', defaults to 'bash')
* @returns {steps} - Array containing a single step object

* @example
* base.step(
* name='Run tests',
* run='pytest tests/',
* env={ 'ENV_VAR': 'value' },
* workingDirectory='backend',
* )
*
* base.step(
* name='Set up Python',
* run=|||
* python -m venv venv
* source venv/bin/activate
* pip install -r requirements.txt
* |||,
* )
*/
step(name, run, env=null, workingDirectory=null, ifClause=null, id=null, continueOnError=null, shell=null)::
[
{
name: name,
run: run,
} + (if workingDirectory != null then { 'working-directory': workingDirectory } else {})
+ (if env != null then { env: env } else {})
+ (if ifClause != null then { 'if': ifClause } else {})
+ (if id != null then { id: id } else {})
+ (if continueOnError == null then {} else { 'continue-on-error': continueOnError })
+ (if shell == null then {} else { 'shell': shell }),
],

/**
* Creates a GitHub Actions step that uses a predefined action from the marketplace or repository.
* Security: Prefer pinning action references to a full commit SHA (e.g., actions/checkout@<commit_sha>) instead of a mutable tag/version,
* especially for lesser-known or smaller third-party actions to reduce supply chain attack risk.
*
* @docs https://docs.github.com/en/actions/reference/workflows-and-actions/workflow-syntax#jobsjob_idsteps
*
* @param {string} name - Display name for the step in the GitHub UI
* @param {string} uses - The action to use (e.g., 'actions/checkout@v6', './path/to/action')
* @param {object} [env=null] - Environment variables for this step
* @param {object} [with=null] - Input parameters to pass to the action
* @param {string} [id=null] - Unique identifier for this step (used to reference outputs)
* @param {string} [ifClause=null] - Conditional expression to determine if step should run
* @param {boolean} [continueOnError=null] - Whether to continue job if this step fails
* @param {number} [timeoutMinutes=null] - Maximum minutes to run this step before failing
* @returns {steps} - Array containing a single step object
*/
action(name, uses, env=null, with=null, id=null, ifClause=null, continueOnError=null, timeoutMinutes=null)::
[
{
name: name,
uses: uses,
} + (if env != null then { env: env } else {})
+ (if with != null && with != {} then { with: with } else {})
+ (if id != null then { id: id } else {})
+ (if ifClause != null then { 'if': ifClause } else {})
+ (if continueOnError == null then {} else { 'continue-on-error': continueOnError })
+ (if timeoutMinutes == null then {} else { 'timeout-minutes': timeoutMinutes })
],
}
148 changes: 148 additions & 0 deletions .github/jsonnet/buckets.jsonnet
Original file line number Diff line number Diff line change
@@ -0,0 +1,148 @@
{
/**
* Uploads all files in the source folder to the destination bucket, including compression and TTL headers.
*
* WARNINGS:
* - Remote/destination files not included in the source will be DELETED recursively if pruneRemote is true!
* - The files in the source directory will be modified. Do not attempt to use this directory after running this command.
* - Must be run with bash shell.
*
* @param {string} sourcePath - The source directory to upload. Can be a local folder or a path in a bucket, depending on sourceBucket. Required.
* @param {string} [sourceBucket=null] - The source bucket. If null, the sourcePath is a local directory.
* @param {string} destinationBucket - The destination bucket. Required.
* @param {string} destinationPath - The destination directory in the bucket. Required.
* @param {boolean} [pruneRemote=false] - If true, all files in the destination bucket that are not in the source will be deleted. Can only be used with destinationPath containing 'pr-'.
* @param {array} [compressFileExtentions=['css', 'svg', 'html', 'json', 'js', 'xml', 'txt', 'map']] - A list of file extensions that will be compressed. Set to an empty list to disable compression.
* @param {number} [compressJobs=4] - The number of parallel gzip compression jobs. Use 4 for arc-runner-2 and 16 for arc-runner-16.
* @param {array|string} [lowTTLfiles=[]] - A list of files, or a single regex, that will be uploaded with a low TTL. Use this for files that are not fingerprinted.
* @param {number} [lowTTL=60] - The TTL for lowTTLfiles in seconds.
* @param {number} [lowTTLStaleWhileRevalidate=60] - The stale-while-revalidate value for lowTTLfiles in seconds.
* @param {string} [lowTTLHeader] - The Cache-Control header for lowTTLfiles. This is generated from lowTTL and lowTTLStaleWhileRevalidate.
* @param {number} [highTTL=604800] - The TTL for all other files in seconds (defaults to 1 week).
* @param {number} [highTTLStaleWhileRevalidate=86400] - The stale-while-revalidate value for all other files in seconds (defaults to 1 day).
* @param {string} [highTTLHeader] - The Cache-Control header for all other files. This is generated from highTTL and highTTLStaleWhileRevalidate.
* @param {array} [additionalHeaders=[]] - Additional headers to add to all uploaded files. This should be an array of strings.
* @returns {string} - Complete bash command for uploading files to Google Cloud Storage with compression and caching
*/
uploadFilesToBucketCommand(
sourcePath,
sourceBucket=null,
destinationBucket,
destinationPath,
pruneRemote=false,
compressFileExtentions=['css', 'svg', 'html', 'json', 'js', 'xml', 'txt', 'map'],
compressJobs=4,
lowTTLfiles=[],
lowTTL=60,
lowTTLStaleWhileRevalidate=60,
lowTTLHeader='Cache-Control: public, max-age=' + lowTTL + (if lowTTLStaleWhileRevalidate == 0 then '' else ', stale-while-revalidate=' + lowTTLStaleWhileRevalidate),
highTTL=604800, // 1 week
highTTLStaleWhileRevalidate=86400, // 1 day
highTTLHeader='Cache-Control: public, max-age=' + highTTL + (if highTTLStaleWhileRevalidate == 0 then '' else ', stale-while-revalidate=' + highTTLStaleWhileRevalidate),
additionalHeaders=[],
)::
// if this function is called with remote pruning, destination must contain pr-
assert !pruneRemote || std.length(std.findSubstr('/pr-', destinationPath)) > 0;

local hasLowTTLfiles = (std.isArray(lowTTLfiles) && std.length(lowTTLfiles) > 0) || (std.isString(lowTTLfiles) && lowTTLfiles != '');
local lowTTLfilesRegex = if std.isArray(lowTTLfiles) then '(' + std.strReplace(std.join('|', lowTTLfiles), '.', '\\.') + ')' else lowTTLfiles;
local highTTLfilesRegex = '(?!' + lowTTLfilesRegex + ').*';

local hasCompressedFiles = (std.isArray(compressFileExtentions) && std.length(compressFileExtentions) > 0) || (std.isString(compressFileExtentions) && compressFileExtentions != '');
local compressedFilesRegex = '(' + std.join('|', std.map(function(ext) '(.*\\.' + ext + ')', compressFileExtentions)) + ')';
local uncompressedFilesRegex = '(?!' + compressedFilesRegex + ').*';

local compressionHeader = 'Content-Encoding: gzip';


local rsyncCommand = function(name, excludeRegexes, headers)
local excludeRegex = if std.length(excludeRegexes) == 0 then null else '^((' + std.join(')|(', excludeRegexes) + '))$';

'echo "Uploading ' + name + ' files"\n' +
'gsutil -m ' + std.join(' ', std.map(function(header) '-h "' + header + '" ', headers + additionalHeaders)) + 'rsync -r -c' +
(if excludeRegex == null then '' else ' -x "' + excludeRegex + '"') +
(if pruneRemote then ' -d' else '') +
(if sourceBucket == null then ' ./' else ' gs://' + sourceBucket + '/' + sourcePath + '/') +
' gs://' + destinationBucket + '/' + destinationPath + '/;\n' +
'echo "Uploading ' + name + ' files completed"; echo\n' +
'\n';

'set -e -o pipefail;\n' +
(if sourceBucket == null then 'cd ' + sourcePath + ';\n' else '') +
'\n' +


if hasCompressedFiles then
(
if sourceBucket == null then
'echo "Compressing files in parallel before uploading"\n' +
'{\n' +
" for file in `find . -type f -regextype posix-egrep -regex '" + compressedFilesRegex + "' | sed --expression 's/\\.\\///g'`; do\n" +
' echo "gzip -9 $file; mv $file.gz $file"\n' +
' done\n' +
'} | parallel --halt now,fail=1 -j ' + compressJobs + '\n' +
'echo "Compressing files in parallel completed"\n' +
'\n'
else ''
) +

if hasLowTTLfiles then
rsyncCommand(
'highTTL compressed',
excludeRegexes=[lowTTLfilesRegex, uncompressedFilesRegex],
headers=[highTTLHeader, compressionHeader],
) +
rsyncCommand(
'highTTL uncompressed',
excludeRegexes=[lowTTLfilesRegex, compressedFilesRegex],
headers=[highTTLHeader],
) +

rsyncCommand(
'lowTTL compressed',
excludeRegexes=[highTTLfilesRegex, uncompressedFilesRegex],
headers=[lowTTLHeader, compressionHeader],
) +
rsyncCommand(
'lowTTL uncompressed',
excludeRegexes=[highTTLfilesRegex, compressedFilesRegex],
headers=[lowTTLHeader],
)


else // no lowTTL files, with compression
rsyncCommand(
'compressed',
excludeRegexes=[uncompressedFilesRegex],
headers=[highTTLHeader, compressionHeader],
) +
rsyncCommand(
'uncompressed',
excludeRegexes=[compressedFilesRegex],
headers=[highTTLHeader],
)


else // no compression
if hasLowTTLfiles then
rsyncCommand(
'highTTL',
excludeRegexes=[lowTTLfilesRegex],
headers=[highTTLHeader],
) +

rsyncCommand(
'lowTTL',
excludeRegexes=[highTTLfilesRegex],
headers=[lowTTLHeader],
)


else // no lowTTL files, no compression
rsyncCommand(
'all',
excludeRegexes=[],
headers=[highTTLHeader],
),

}
Loading
Loading