Skip to content

Commit

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.

Claude-Session:
https://claude.ai/code/session_01PnwTRSNaC7vEHTAuBzjhrn
Claudeagent and Claudeagentcommitted The first commitBrowse files
4 files+292−00/4 viewed
+75−0
1+# g1t/deploy
2+
3+Builds the checkout a g1t Actions job is in and puts it on
4+[g1t.page](https://docs.g1t.sh/guides/deployments/): production when the
5+run is on the project's production branch, the pull request's preview for
6+a pull request's run, and the branch's preview for a run on any other
7+branch.
8+
9+```yaml
10+name: Deploy
11+
12+on:
13+ push:
14+ branches: [main]
15+ pull_request:
16+
17+permissions:
18+ contents: read
19+ deployments: write
20+
21+jobs:
22+ deploy:
23+ runs-on: ubuntu-latest
24+ steps:
25+ - uses: actions/checkout@v5
26+ - id: deploy
27+ uses: g1t/deploy@v1
28+ - run: echo "Live at ${{ steps.deploy.outputs.url }}"
29+```
30+
31+The project must have Deployments on, and the workspace's plan must
32+include them. The guide,
33+[Deploying from a workflow](https://docs.g1t.sh/guides/deployments/#deploying-from-a-workflow),
34+says what is built and when.
35+
36+## Inputs
37+
38+| Input | Default | |
39+| --- | --- | --- |
40+| `project` | The repository's one project with Deployments on | The project's slug. Needed when the repository has several. |
41+| `root-dir` | The project's setting | Where the project is in the checkout. |
42+| `build-command` | The project's setting, else what the build finds | What builds it. |
43+| `output-dir` | The project's setting, else the first of `dist`, `build`, `out`, `public`, `_site`, `.output/public` | The folder a static build writes. |
44+| `sha` | The run's commit | The commit this is a build of, for its `g1t / deploy` check. |
45+| `token` | `${{ github.token }}` | The job's own token. It needs `deployments: write`; any other token is refused. |
46+
47+## Outputs
48+
49+| Output | |
50+| --- | --- |
51+| `url` | Where the app is served. |
52+| `deployment-id` | The build's id, `dpl_…`. |
53+| `kind` | `production` or `preview`. |
54+
55+## How it works
56+
57+1. It calls `POST /repos/{owner}/{repo}/deployments/builds` with the job's
58+ token. g1t checks the plan and the spend limit, decides production or
59+ preview from the run, sets the commit's `g1t / deploy` check to pending,
60+ and answers with a token for this build alone and the project's
61+ variables and secrets for builds of that kind.
62+2. It masks that token and every secret with `::add-mask::`.
63+3. It runs `g1t-runner deploy` in the checkout, which installs, builds and
64+ uploads the app exactly as g1t's own builds do, and reports to g1t with
65+ the build's token. The build's log is in the step's.
66+
67+A pull request from a fork or from someone without the Write role, and a
68+run that had to be approved, build without the project's secrets. The
69+build's time is the job's Actions time; it is not charged again.
70+
71+`g1t-runner` is on g1t's machines. Elsewhere (a self-hosted runner), the
72+action uses the one on `PATH`, or downloads the newest release from
73+`https://g1t.sh/downloads/runner/latest/` and checks it against the
74+release's `SHA256SUMS`. It runs on Linux and macOS, with `curl` and either
75+`jq` or `python3`.
+54−0
1+name: Deploy to g1t.page
2+description: Builds this checkout and puts it on g1t.page. Production from the project's production branch, a preview from a pull request or any other branch.
3+author: g1t
4+
5+inputs:
6+ project:
7+ description: The project's slug. Leave it out when the repository has one project with Deployments on.
8+ required: false
9+ default: ""
10+ root-dir:
11+ description: Where the project is in the checkout. The project's own setting unless you say.
12+ required: false
13+ default: ""
14+ build-command:
15+ description: What builds it. The project's own setting, else what the build finds (a Workers config, or the package's build script).
16+ required: false
17+ default: ""
18+ output-dir:
19+ description: The folder a static build writes. The project's own setting, else the first of dist, build, out, public, _site or .output/public.
20+ required: false
21+ default: ""
22+ sha:
23+ description: The commit this is a build of, for the commit's g1t / deploy check. The run's commit unless you say.
24+ required: false
25+ default: ""
26+ token:
27+ description: The job's own token, which needs permissions deployments write. Another token is refused.
28+ required: false
29+ default: ${{ github.token }}
30+
31+outputs:
32+ url:
33+ description: Where the app is served, on g1t.page.
34+ value: ${{ steps.deploy.outputs.url }}
35+ deployment-id:
36+ description: The build's id, dpl_….
37+ value: ${{ steps.deploy.outputs.deployment-id }}
38+ kind:
39+ description: production or preview.
40+ value: ${{ steps.deploy.outputs.kind }}
41+
42+runs:
43+ using: composite
44+ steps:
45+ - id: deploy
46+ shell: bash
47+ env:
48+ INPUT_PROJECT: ${{ inputs.project }}
49+ INPUT_ROOT_DIR: ${{ inputs.root-dir }}
50+ INPUT_BUILD_COMMAND: ${{ inputs.build-command }}
51+ INPUT_OUTPUT_DIR: ${{ inputs.output-dir }}
52+ INPUT_SHA: ${{ inputs.sha }}
53+ INPUT_TOKEN: ${{ inputs.token }}
54+ run: bash "$GITHUB_ACTION_PATH/deploy.sh"
+109−0
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.
6+set -euo pipefail
7+
8+api="${GITHUB_API_URL:-https://api.g1t.sh}"
9+api="${api%/}"
10+repo="${GITHUB_REPOSITORY:?GITHUB_REPOSITORY is not set: run this in a g1t Actions job.}"
11+if [ -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
14+fi
15+
16+# JSON, with jq where there is one, else Python.
17+if 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 != ""))'; }
22+elif command -v python3 >/dev/null 2>&1; then
23+ field() {
24+ python3 -c 'import json, sys
25+v = json.load(sys.stdin).get(sys.argv[1])
26+if v is None: sys.exit(0)
27+print(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
30+for v in (json.load(sys.stdin).get(sys.argv[1]) or {}).values(): print(v)' "$1"; }
31+ message() { python3 -c 'import json, sys
32+d = 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
34+print(json.dumps({k: v for k, v in (("project", sys.argv[1]), ("sha", sys.argv[2])) if v}))' "$1" "$2"; }
35+else
36+ echo "::error::g1t/deploy needs jq or python3 on the machine."
37+ exit 1
38+fi
39+
40+# The build, opened for this job.
41+request="$(body "${INPUT_PROJECT:-}" "${INPUT_SHA:-}")"
42+answer_file="$(mktemp)"
43+trap 'rm -f "${answer_file}"' EXIT
44+status="$(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+}
53+answer="$(cat "${answer_file}")"
54+if [ "${status}" != "200" ]; then
55+ why="$(message <<<"${answer}")"
56+ echo "::error::g1t did not open the build (${status}): ${why:-no reason given}"
57+ exit 1
58+fi
59+
60+# Nothing secret is printed before it is masked.
61+build_token="$(field token <<<"${answer}")"
62+echo "::add-mask::${build_token}"
63+while IFS= read -r secret; do
64+ [ -n "${secret}" ] && echo "::add-mask::${secret}"
65+done < <(values build_secrets <<<"${answer}")
66+
67+deployment_id="$(field deploy_id <<<"${answer}")"
68+kind="$(field kind <<<"${answer}")"
69+url="$(field url <<<"${answer}")"
70+project="$(field project <<<"${answer}")"
71+branch="$(field branch <<<"${answer}")"
72+trusted="$(field trusted <<<"${answer}")"
73+{
74+ echo "deployment-id=${deployment_id}"
75+ echo "kind=${kind}"
76+ echo "url=${url}"
77+} >>"${GITHUB_OUTPUT:-/dev/null}"
78+
79+if [ "${kind}" = "production" ]; then
80+ echo "Building ${project} for production, at ${url}"
81+else
82+ echo "Building the ${branch} preview of ${project}, at ${url}"
83+fi
84+if [ "${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."
86+fi
87+
88+# What the step gives wins over the project's own settings.
89+pick() { if [ -n "$1" ]; then echo "$1"; else field "$2" <<<"${answer}"; fi; }
90+export DEPLOY_ID="${deployment_id}"
91+export DEPLOY_TOKEN="${build_token}"
92+export G1T_API="${api}"
93+export ROOT_DIR="$(pick "${INPUT_ROOT_DIR:-}" root_dir)"
94+export BUILD_COMMAND="$(pick "${INPUT_BUILD_COMMAND:-}" build_command)"
95+export OUTPUT_DIR="$(pick "${INPUT_OUTPUT_DIR:-}" output_dir)"
96+export BUILD_ENV="$(field build_env <<<"${answer}")"
97+export BUILD_SECRETS="$(field build_secrets <<<"${answer}")"
98+unset GIT_REMOTE
99+
100+runner="$(bash "${GITHUB_ACTION_PATH:-$(dirname "$0")}/runner.sh")"
101+if ! "${runner}" deploy --help 2>/dev/null | grep -q DEPLOY_ID; then
102+ 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."
103+ exit 1
104+fi
105+"${runner}" deploy
106+echo "Deployed: ${url}"
107+if [ -n "${GITHUB_STEP_SUMMARY:-}" ]; then
108+ echo "Deployed ${project} (${kind}) to ${url}" >>"${GITHUB_STEP_SUMMARY}"
109+fi
+54−0
1+#!/usr/bin/env bash
2+# Finds g1t-runner for the g1t/deploy and g1t/detect actions and prints its
3+# path: G1T_RUNNER if it names one, else the one on PATH (g1t's machines
4+# have it at /usr/local/bin/g1t-runner), else the newest release from
5+# g1t.sh/downloads/runner, checked against that release's SHA256SUMS, as
6+# docs.g1t.sh/guides/self-hosted-runners/ downloads it.
7+#
8+# actions/deploy/runner.sh and actions/detect/runner.sh are the same file:
9+# each action is published as a repository of its own.
10+set -euo pipefail
11+
12+if [ -n "${G1T_RUNNER:-}" ] && [ -x "${G1T_RUNNER}" ]; then
13+ echo "${G1T_RUNNER}"
14+ exit 0
15+fi
16+if found="$(command -v g1t-runner 2>/dev/null)"; then
17+ echo "${found}"
18+ exit 0
19+fi
20+
21+case "$(uname -s)-$(uname -m)" in
22+ Linux-x86_64 | Linux-amd64) platform=linux-x64 ;;
23+ Linux-aarch64 | Linux-arm64) platform=linux-arm64 ;;
24+ Darwin-arm64) platform=macos-arm64 ;;
25+ Darwin-x86_64) platform=macos-x64 ;;
26+ *)
27+ echo "::error::g1t-runner is not on PATH, and g1t has no release of it for $(uname -s) $(uname -m)." >&2
28+ exit 1
29+ ;;
30+esac
31+
32+downloads="${G1T_RUNNER_DOWNLOADS:-https://g1t.sh/downloads/runner/latest}"
33+dir="${RUNNER_TEMP:-${TMPDIR:-/tmp}}/g1t-runner-${platform}"
34+file="g1t-runner-${platform}"
35+if [ ! -x "${dir}/g1t-runner" ]; then
36+ mkdir -p "${dir}"
37+ echo "Downloading ${file} from ${downloads}" >&2
38+ curl -fsSL --retry 3 -o "${dir}/${file}" "${downloads}/${file}"
39+ curl -fsSL --retry 3 -o "${dir}/SHA256SUMS" "${downloads}/SHA256SUMS"
40+ expected="$(awk -v name="${file}" '$2 == name { print $1 }' "${dir}/SHA256SUMS")"
41+ if command -v sha256sum >/dev/null 2>&1; then
42+ actual="$(sha256sum "${dir}/${file}" | awk '{ print $1 }')"
43+ else
44+ actual="$(shasum -a 256 "${dir}/${file}" | awk '{ print $1 }')"
45+ fi
46+ if [ -z "${expected}" ] || [ "${expected}" != "${actual}" ]; then
47+ echo "::error::The downloaded ${file} does not match the release's SHA256SUMS." >&2
48+ rm -f "${dir}/${file}"
49+ exit 1
50+ fi
51+ chmod +x "${dir}/${file}"
52+ mv "${dir}/${file}" "${dir}/g1t-runner"
53+fi
54+echo "${dir}/g1t-runner"