Skip to content
111 linesCodeBlameRaw

Pick any line to see why it is the way it is: the commit, the pull request and issue it came from, and what the agent was thinking.

g1t has actions of its own: g1t/deploy builds the checkout a job is in and puts it on g1t.page, opening the build with the job's token, masking the build's token and the project's secrets, and running g1t-runner deploy, with the app's url, the deployment's id and its kind as outputs; g1t/detect says what such a build would find (kind, install, build and output-dir) without building anything. Both use the g1t-runner on the machine, or download the newest release from g1t.sh/downloads/runner and check it against its SHA256SUMS. They live under actions/, and the Actions release workflow pushes each folder to its own repository in the g1t workspace and moves its v1 tag whenever main changes them, with the G1T_ACTIONS_PUBLISH_TOKEN secret its header describes.1#!/usr/bin/env bash
2# The g1t/deploy action's work (see README.md): opens a g1t.page build for
3# this job with POST /repos/{owner}/{repo}/deployments/builds, masks its
4# token and the project's build secrets, and runs `g1t-runner deploy` in
5# the checkout with them. Reads the action's inputs as INPUT_* variables.
6set -euo pipefail
7
8api="${GITHUB_API_URL:-https://api.g1t.sh}"
9api="${api%/}"
10repo="${GITHUB_REPOSITORY:?GITHUB_REPOSITORY is not set: run this in a g1t Actions job.}"
11if [ -z "${INPUT_TOKEN:-}" ]; then
12 echo "::error::The token input is empty: give the job's own token, \${{ github.token }}, with permissions: deployments: write."
13 exit 1
14fi
15
16# JSON, with jq where there is one, else Python.
17if command -v jq >/dev/null 2>&1; then
18 field() { jq -r --arg key "$1" '.[$key] // empty | if type == "object" or type == "array" then tojson else tostring end'; }
19 values() { jq -r --arg key "$1" '.[$key] // {} | .[] | tostring'; }
20 message() { jq -r '.message // .error.message // empty' 2>/dev/null || true; }
21 body() { jq -cn --arg project "$1" --arg sha "$2" '{project: $project, sha: $sha} | with_entries(select(.value != ""))'; }
22elif command -v python3 >/dev/null 2>&1; then
23 field() {
24 python3 -c 'import json, sys
25v = json.load(sys.stdin).get(sys.argv[1])
26if v is None: sys.exit(0)
27print(json.dumps(v, separators=(",", ":")) if isinstance(v, (dict, list)) else str(v).lower() if isinstance(v, bool) else v)' "$1"
28 }
29 values() { python3 -c 'import json, sys
30for v in (json.load(sys.stdin).get(sys.argv[1]) or {}).values(): print(v)' "$1"; }
31 message() { python3 -c 'import json, sys
32d = json.load(sys.stdin); print(d.get("message") or (d.get("error") or {}).get("message") or "")' 2>/dev/null || true; }
33 body() { python3 -c 'import json, sys
34print(json.dumps({k: v for k, v in (("project", sys.argv[1]), ("sha", sys.argv[2])) if v}))' "$1" "$2"; }
35else
36 echo "::error::g1t/deploy needs jq or python3 on the machine."
37 exit 1
38fi
39
40# The build, opened for this job.
41request="$(body "${INPUT_PROJECT:-}" "${INPUT_SHA:-}")"
42answer_file="$(mktemp)"
43trap 'rm -f "${answer_file}"' EXIT
44status="$(curl -sS -o "${answer_file}" -w '%{http_code}' -X POST \
45 -H "Authorization: Bearer ${INPUT_TOKEN}" \
46 -H "Content-Type: application/json" \
47 -H "Accept: application/json" \
48 --data "${request}" \
49 "${api}/repos/${repo}/deployments/builds")" || {
50 echo "::error::Could not reach ${api} to open the build."
51 exit 1
52}
53answer="$(cat "${answer_file}")"
54if [ "${status}" != "200" ]; then
55 why="$(message <<<"${answer}")"
56 echo "::error::g1t did not open the build (${status}): ${why:-no reason given}"
57 exit 1
58fi
59
60# Nothing secret is printed before it is masked.
61build_token="$(field token <<<"${answer}")"
62echo "::add-mask::${build_token}"
63while IFS= read -r secret; do
64 [ -n "${secret}" ] && echo "::add-mask::${secret}"
65done < <(values build_secrets <<<"${answer}")
66
67deployment_id="$(field deploy_id <<<"${answer}")"
68kind="$(field kind <<<"${answer}")"
69url="$(field url <<<"${answer}")"
70project="$(field project <<<"${answer}")"
71branch="$(field branch <<<"${answer}")"
72trusted="$(field trusted <<<"${answer}")"
73{
74 echo "deployment-id=${deployment_id}"
75 echo "kind=${kind}"
76 echo "url=${url}"
77} >>"${GITHUB_OUTPUT:-/dev/null}"
78
79if [ "${kind}" = "production" ]; then
80 echo "Building ${project} for production, at ${url}"
81else
82 echo "Building the ${branch} preview of ${project}, at ${url}"
83fi
84if [ "${trusted}" != "true" ]; then
85 echo "::notice::This run is not trusted with the project's build secrets (a pull request from outside, or a run that needed approval): it builds without them."
86fi
87
88# What the step gives wins over the project's own settings.
89pick() { if [ -n "$1" ]; then echo "$1"; else field "$2" <<<"${answer}"; fi; }
90export DEPLOY_ID="${deployment_id}"
91export DEPLOY_TOKEN="${build_token}"
92export G1T_API="${api}"
93export ROOT_DIR="$(pick "${INPUT_ROOT_DIR:-}" root_dir)"
94export BUILD_COMMAND="$(pick "${INPUT_BUILD_COMMAND:-}" build_command)"
95export OUTPUT_DIR="$(pick "${INPUT_OUTPUT_DIR:-}" output_dir)"
g1t.page runs Docker deployments, behind a switch that is off until Cloudflare's container APIs are checked against g1t's account (`CONTAINERS` in the deployments service). Each container app is a Worker of g1t's own in the same Workers for Platforms namespace, so the dispatcher, custom domains, previews and take-downs reach it by name as any app: its `G1tApp` Durable Object holds a Container application (default scheduling policy, one instance, the project's **Container size**, `lite` unless chosen), starts it on the first request with the project's secrets and variables and `PORT` as its environment, waits up to 90 seconds for it to answer on its port, proxies to it, and stops it five minutes after the last request; it charges each minute awake ahead and the bytes it sends, and reports them with a token of its own to `POST /deployments/apps/<app>/meter`, which the API forwards. A build's new `registry` step gets a registry login g1t mints push-only for 15 minutes, with its app's own repository named; g1t runs only an image by digest in that repository, checked with the registry, and never hands a build a login that can pull. Container time is metered at Cloudflare's Containers price plus the margin through new price book meters (vCPU charged as busy while awake, and egress at the most a GB costs anywhere, so g1t never sells below cost), noted for the spend limit as it runs and charged with the month's Deployments line; a paused workspace's container apps stop and start no more. Settings, Deployments gets Build as (Auto, Static site, Workers, Docker), Dockerfile and Container size with each size's price an hour, and says what the last build found with what it was built with ("Static site · Astro", "Container · Dockerfile"), now that the finish report's `framework` is stored. Deploy sandboxes get Docker, `BUILD_AS` and `DOCKERFILE`, and reach `registry.cloudflare.com`; `start_deployment_build` answers `build_as` and `dockerfile` for `g1t/deploy`, which takes them as inputs, as `g1t/detect` does. The Deployments guide has Docker deployments, their prices and how they work with the evidence for it, the limitations page says what is left, and Deploy g1t to Cloudflare lists each live check to make before switching it on.96export BUILD_AS="$(pick "${INPUT_BUILD_AS:-}" build_as)"
97export DOCKERFILE="$(pick "${INPUT_DOCKERFILE:-}" dockerfile)"
g1t has actions of its own: g1t/deploy builds the checkout a job is in and puts it on g1t.page, opening the build with the job's token, masking the build's token and the project's secrets, and running g1t-runner deploy, with the app's url, the deployment's id and its kind as outputs; g1t/detect says what such a build would find (kind, install, build and output-dir) without building anything. Both use the g1t-runner on the machine, or download the newest release from g1t.sh/downloads/runner and check it against its SHA256SUMS. They live under actions/, and the Actions release workflow pushes each folder to its own repository in the g1t workspace and moves its v1 tag whenever main changes them, with the G1T_ACTIONS_PUBLISH_TOKEN secret its header describes.98export BUILD_ENV="$(field build_env <<<"${answer}")"
99export BUILD_SECRETS="$(field build_secrets <<<"${answer}")"
100unset GIT_REMOTE
101
102runner="$(bash "${GITHUB_ACTION_PATH:-$(dirname "$0")}/runner.sh")"
103if ! "${runner}" deploy --help 2>/dev/null | grep -q DEPLOY_ID; then
104 echo "::error::${runner} is older than g1t/deploy needs: update it (g1t-runner update), or let the action download it by taking it off PATH."
105 exit 1
106fi
107"${runner}" deploy
108echo "Deployed: ${url}"
109if [ -n "${GITHUB_STEP_SUMMARY:-}" ]; then
110 echo "Deployed ${project} (${kind}) to ${url}" >>"${GITHUB_STEP_SUMMARY}"
111fi