Skip to content

Commit

Merge branch 'worktree-agent-a4d35ee70c50683d0' into claude/cool-newton-7nm8r9

# Conflicts: # apps/docs/src/content/docs/guides/actions.md

Claudeagentcommitted Parents52466f6f025d21Browse files
30 files+697−40/30 viewed
+67−0
1+# Publishes g1t's own actions: each folder of actions/ (deploy, detect…)
2+# is pushed, as the whole of its own history, to the repository of the
3+# same name in the g1t workspace on g1t.sh (actions/deploy to g1t/deploy),
4+# and its major tag, v1, is moved to it. Workflows anywhere use them as
5+# `uses: g1t/deploy@v1`. A change to an action that breaks how it is
6+# used gets a new major tag here (MAJOR below), and the old one stays.
7+#
8+# Runs on a push to main that changes actions/, and by hand.
9+#
10+# Needs the repository secret G1T_ACTIONS_PUBLISH_TOKEN: an access token of
11+# the g1t workspace with contents: write on its action repositories
12+# (g1t/deploy, g1t/detect, and one for each new folder, made public
13+# before its first release). The job's own token reaches this repository
14+# only, so it cannot push there.
15+name: Actions release
16+
17+on:
18+ push:
19+ branches: [main]
20+ paths:
21+ - actions/**
22+ workflow_dispatch:
23+
24+permissions:
25+ contents: read
26+
27+concurrency:
28+ group: actions-release
29+ cancel-in-progress: false
30+
31+env:
32+ MAJOR: v1
33+ NAMESPACE: g1t
34+
35+jobs:
36+ publish:
37+ name: Publish g1t/<action>
38+ runs-on: ubuntu-latest
39+ timeout-minutes: 15
40+ steps:
41+ - uses: actions/checkout@v5
42+ with:
43+ fetch-depth: 0
44+ - name: Push each action and move its major tag
45+ env:
46+ PUBLISH_TOKEN: ${{ secrets.G1T_ACTIONS_PUBLISH_TOKEN }}
47+ SERVER: ${{ github.server_url }}
48+ run: |
49+ set -euo pipefail
50+ if [ -z "${PUBLISH_TOKEN}" ]; then
51+ echo "::error::Set the repository secret G1T_ACTIONS_PUBLISH_TOKEN (this workflow's header says what it needs)."
52+ exit 1
53+ fi
54+ echo "::add-mask::${PUBLISH_TOKEN}"
55+ auth="Authorization: Basic $(printf 'g1t:%s' "${PUBLISH_TOKEN}" | base64 -w0)"
56+ git config user.name g1t
57+ git config user.email g1t@users.noreply.g1t.sh
58+ for dir in actions/*/; do
59+ name="$(basename "${dir}")"
60+ [ -f "${dir}action.yml" ] || { echo "::warning::${dir} has no action.yml; skipped."; continue; }
61+ commit="$(git subtree split --prefix "actions/${name}" HEAD)"
62+ remote="${SERVER}/${NAMESPACE}/${name}.git"
63+ echo "Publishing actions/${name} (${commit}) to ${NAMESPACE}/${name}"
64+ git -c http.extraHeader="${auth}" push "${remote}" "${commit}:refs/heads/main"
65+ git -c http.extraHeader="${auth}" push --force "${remote}" "${commit}:refs/tags/${MAJOR}"
66+ echo "- \`${NAMESPACE}/${name}@${MAJOR}\` is ${commit}" >> "${GITHUB_STEP_SUMMARY}"
67+ done
+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"
+35−0
1+# g1t/detect
2+
3+Says what a [g1t.page](https://docs.g1t.sh/guides/deployments/) build of
4+the checkout would find, without building or deploying anything: the
5+same rules [g1t/deploy](https://g1t.sh/g1t/deploy) builds with.
6+
7+```yaml
8+steps:
9+ - uses: actions/checkout@v5
10+ - id: detect
11+ uses: g1t/detect@v1
12+ - if: steps.detect.outputs.kind == 'none'
13+ run: echo "::error::Nothing here would be served."
14+ - if: steps.detect.outputs.install != ''
15+ run: ${{ steps.detect.outputs.install }}
16+```
17+
18+## Inputs
19+
20+| Input | Default | |
21+| --- | --- | --- |
22+| `root-dir` | The checkout | Where the project is in the checkout. |
23+| `build-command` | None | A build command of your own, as `g1t/deploy` takes it. |
24+| `output-dir` | None | An output folder of your own, as `g1t/deploy` takes it. |
25+
26+## Outputs
27+
28+| Output | |
29+| --- | --- |
30+| `kind` | `workers` (a `wrangler.jsonc`, `wrangler.json` or `wrangler.toml`), `static` (a site a build writes), `html` (the files as they are) or `none`. |
31+| `install` | How dependencies are installed, from the lockfile: `npm ci`, `pnpm install --frozen-lockfile`, and so on. Empty without a `package.json`. |
32+| `build` | What builds it: your command, `npm run build` for a package with a build script, or `wrangler deploy --dry-run` for Workers. Empty when nothing does. |
33+| `output-dir` | The folder served, relative to the project (`.` for the project itself): a Workers project's assets directory, or the first of `dist`, `build`, `out`, `public`, `_site`, `.output/public` that exists. Empty when a build has yet to write it. |
34+
35+It runs `g1t-runner detect`, found or downloaded as `g1t/deploy` finds it.
+47−0
1+name: Detect a g1t.page project
2+description: Says what a g1t.page build of this checkout would find, without building anything. Its kind, how it installs, what builds it and where its output is.
3+author: g1t
4+
5+inputs:
6+ root-dir:
7+ description: Where the project is in the checkout. The checkout itself unless you say.
8+ required: false
9+ default: ""
10+ build-command:
11+ description: A build command of your own, as g1t/deploy takes it.
12+ required: false
13+ default: ""
14+ output-dir:
15+ description: An output folder of your own, as g1t/deploy takes it.
16+ required: false
17+ default: ""
18+
19+outputs:
20+ kind:
21+ description: workers (a Wrangler config), static (a site a build writes), html (the files as they are) or none.
22+ value: ${{ steps.detect.outputs.kind }}
23+ install:
24+ description: How its dependencies are installed, such as npm ci. Empty without a package.json.
25+ value: ${{ steps.detect.outputs.install }}
26+ build:
27+ description: What builds it. Empty when nothing does.
28+ value: ${{ steps.detect.outputs.build }}
29+ output-dir:
30+ description: The folder that is served, relative to the project (. for the project itself). Empty when a build has yet to write it.
31+ value: ${{ steps.detect.outputs.output-dir }}
32+
33+runs:
34+ using: composite
35+ steps:
36+ - id: detect
37+ shell: bash
38+ env:
39+ ROOT_DIR: ${{ inputs.root-dir }}
40+ BUILD_COMMAND: ${{ inputs.build-command }}
41+ OUTPUT_DIR: ${{ inputs.output-dir }}
42+ run: |
43+ set -euo pipefail
44+ runner="$(bash "$GITHUB_ACTION_PATH/runner.sh")"
45+ found="$("$runner" detect)"
46+ echo "$found"
47+ echo "$found" >> "$GITHUB_OUTPUT"
+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"
+33−2
2424 CreateDeploymentStatus,
2525 ListEnvironments,
2626 GetEnvironment,
27+ StartDeploymentBuild,
2728 }
2829
2930 /// The states a deployment's status can have.
3334 /// Every one: `Op::ALL` lists each as `Op::Deployments(…)`, which a
3435 /// test checks against this.
3536 #[cfg(test)]
36− pub const ALL: [DeploymentsOp; 7] = [
37+ pub const ALL: [DeploymentsOp; 8] = [
3738 DeploymentsOp::ListDeployments,
3839 DeploymentsOp::GetDeployment,
3940 DeploymentsOp::CreateDeployment,
4142 DeploymentsOp::CreateDeploymentStatus,
4243 DeploymentsOp::ListEnvironments,
4344 DeploymentsOp::GetEnvironment,
45+ DeploymentsOp::StartDeploymentBuild,
4446 ];
4547
4648 pub fn name(self) -> &'static str {
5254 DeploymentsOp::CreateDeploymentStatus => "create_deployment_status",
5355 DeploymentsOp::ListEnvironments => "list_environments",
5456 DeploymentsOp::GetEnvironment => "get_environment",
57+ DeploymentsOp::StartDeploymentBuild => "start_deployment_build",
5558 }
5659 }
5760
6568 DeploymentsOp::CreateDeploymentStatus => "Create a deployment status",
6669 DeploymentsOp::ListEnvironments => "List environments",
6770 DeploymentsOp::GetEnvironment => "Get an environment",
71+ DeploymentsOp::StartDeploymentBuild => "Start a g1t.page build from a workflow",
6872 }
6973 }
7074
7781 DeploymentsOp::CreateDeploymentStatus => "Add a status to a reported deployment: state (queued, in_progress, success, failure, error or inactive), description, environment_url (where it is served) and log_url (where its output can be read). The deployment takes its state, and any address it gives. A success with auto_inactive (true unless you say) makes the environment's older successful deployments inactive. The commit's `deploy / <environment>` check follows: pending while queued or in progress, then success, failure or error. A g1t.page build's statuses come from the build and cannot be added to. Needs the Write role.",
7882 DeploymentsOp::ListEnvironments => "List the environments a repository's deployments went to, those people use directly first (production by name before others), then the most recently deployed, and after them those with protection rules but no deployment yet. Each has its name, url (where its current deployment is served), production_environment, transient_environment, deployments_count, latest (its newest deployment, whatever its state), current (its newest successful deployment that is still active) and updated_at, and, when it has rules, protection_rules (required_reviewers, wait_timer, branch_policy), deployment_branch_policy, branch_policies and can_admins_bypass. total_count counts deployments across every environment. Needs the Read role.",
7983 DeploymentsOp::GetEnvironment => "Get one environment by name, matched without regard to case, with its current and latest deployments and its protection rules (see update_environment). An environment with rules but no deployment yet is found too. A name with slashes is URL-encoded in the path. Needs the Read role.",
84+ DeploymentsOp::StartDeploymentBuild => "From a running g1t Actions job, open a g1t.page build of one of the repository's projects, which the job then builds in its own checkout with `g1t-runner deploy` (the g1t/deploy action does both). Only the job's own token (G1T_TOKEN) may call it, with `deployments: write` (or `pages: write`) in its `permissions:`. project is the project's slug; it may be left out when the repository has one project with Deployments on. The run decides what is built: a run on the project's production branch (a push, a dispatch or a schedule there) builds production, a pull request's run builds that pull request's preview, and a run on another branch builds that branch's preview; a tag's run is refused. sha is the commit built, the run's unless you say. A pull request from a fork or from someone without the Write role, and a run that had to be approved, get no build secrets. The workspace's plan must include Deployments and its spend limit must not be reached; the build's time is the job's Actions time and is not charged again. Returns deploy_id and token (DEPLOY_ID and DEPLOY_TOKEN for `g1t-runner deploy`, the token good for this build alone until it finishes), kind (production or preview), project, branch, number, commit, url (the app's address), trusted, the project's root_dir, build_command and output_dir, and build_env and build_secrets (the project's variables and secrets for builds of that kind). The commit's `g1t / deploy` check is pending until the build reports.",
8085 }
8186 }
8287
8388 /// Whether it changes anything.
8489 pub fn writes(self) -> bool {
85− matches!(self, DeploymentsOp::CreateDeployment | DeploymentsOp::CreateDeploymentStatus)
90+ matches!(
91+ self,
92+ DeploymentsOp::CreateDeployment | DeploymentsOp::CreateDeploymentStatus | DeploymentsOp::StartDeploymentBuild
93+ )
8694 }
8795
8896 pub fn input(self) -> Value {
137145 &["repo", "id", "state"],
138146 ),
139147 DeploymentsOp::ListEnvironments => (json!({ "repo": repo }), &["repo"]),
148+ DeploymentsOp::StartDeploymentBuild => (
149+ json!({
150+ "repo": repo,
151+ "project": { "type": "string", "description": "The project's slug; may be left out when the repository has one project with Deployments on." },
152+ "sha": { "type": "string", "description": "The commit built, in hex; the run's commit unless you say." },
153+ }),
154+ &["repo"],
155+ ),
140156 DeploymentsOp::GetEnvironment => (
141157 json!({
142158 "repo": repo,
236252 "create_deployment_status"
237253 }
238254 DeploymentsOp::ListEnvironments => "list_environments",
255+ // The deployments service checks the job's token itself: the
256+ // caller (the job's workspace, with `token.job`) is the actor.
257+ DeploymentsOp::StartDeploymentBuild => {
258+ for key in ["project", "sha"] {
259+ if let Some(value) = input[key].as_str().map(str::trim).filter(|value| !value.is_empty()) {
260+ out.insert(key.into(), value.into());
261+ }
262+ }
263+ "start_from_workflow"
264+ }
239265 DeploymentsOp::GetEnvironment => {
240266 let name = input["environment"].as_str().map(str::trim).filter(|name| !name.is_empty()).ok_or("Name the environment.")?;
241267 out.insert("name".into(), name.into());
294320 assert!(super::args(DeploymentsOp::CreateDeploymentStatus, &json!({ "repo": "acme/web", "id": "dep_1" })).is_err());
295321 let (method, args) = super::args(DeploymentsOp::GetEnvironment, &json!({ "repo": "acme/web", "environment": "review/x" })).unwrap();
296322 assert_eq!((method, args["name"].clone()), ("get_environment", json!("review/x")));
323+ let (method, args) = super::args(DeploymentsOp::StartDeploymentBuild, &json!({ "repo": "acme/web", "project": " web ", "sha": "" })).unwrap();
324+ assert_eq!(method, "start_from_workflow");
325+ assert_eq!(args["project"], json!("web"));
326+ assert!(!args.contains_key("sha"), "an empty sha is the run's");
327+ assert!(DeploymentsOp::StartDeploymentBuild.writes());
297328 }
298329
299330 #[test]
+1−0
460460 Op::Deployments(DeploymentsOp::CreateDeploymentStatus),
461461 Op::Deployments(DeploymentsOp::ListEnvironments),
462462 Op::Deployments(DeploymentsOp::GetEnvironment),
463+ Op::Deployments(DeploymentsOp::StartDeploymentBuild),
463464 ],
464465 ),
465466 (
+2−1
707707 }
708708
709709 impl Op {
710− pub const ALL: [Op; 350] = [
710+ pub const ALL: [Op; 351] = [
711711 Op::Whoami,
712712 Op::GetWorkspace,
713713 Op::CreateWorkspace,
983983 Op::Deployments(DeploymentsOp::CreateDeploymentStatus),
984984 Op::Deployments(DeploymentsOp::ListEnvironments),
985985 Op::Deployments(DeploymentsOp::GetEnvironment),
986+ Op::Deployments(DeploymentsOp::StartDeploymentBuild),
986987 Op::Artifacts(ArtifactsOp::ListArtifacts),
987988 Op::Artifacts(ArtifactsOp::ListRunArtifacts),
988989 Op::Artifacts(ArtifactsOp::GetArtifact),
+30−0
1056710567 },
1056810568 "notes": "Production comes first, then the most recently deployed. `latest` is the newest deployment whatever its state; `current` is the newest that succeeded and is still active, and `url` is where it is served."
1056910569 },
10570+ "start_deployment_build": {
10571+ "params": {
10572+ "owner": "flagon-io",
10573+ "name": "hello"
10574+ },
10575+ "request": {
10576+ "project": "hello"
10577+ },
10578+ "response": {
10579+ "deploy_id": "dpl_01kq9c4e6g8j0m2p4r6t8v0x2z",
10580+ "token": "…",
10581+ "kind": "preview",
10582+ "project": "hello",
10583+ "branch": "new-header",
10584+ "number": 42,
10585+ "commit": "9f2c4e6a8b0d1f3e5a7c9b1d3f5e7a9c0b2d4f6e",
10586+ "url": "https://hello-git-new-header-flagonio.g1t.page",
10587+ "trusted": true,
10588+ "root_dir": "",
10589+ "build_command": null,
10590+ "output_dir": null,
10591+ "build_env": {
10592+ "PUBLIC_API_URL": "https://api.example.com"
10593+ },
10594+ "build_secrets": {
10595+ "SENTRY_AUTH_TOKEN": "…"
10596+ }
10597+ },
10598+ "notes": "Called from a workflow step with the job's own token, `${{ github.token }}`, in a job with `permissions: deployments: write`; the [`g1t/deploy`](/guides/deployments/#deploying-from-a-workflow) action calls it, masks the token and the secrets, and runs `g1t-runner deploy` with them. Any other token is refused. This is a pull request's run, so it builds the pull request's preview; the same workflow on the production branch builds production. See [Deploying from a workflow](/guides/deployments/#deploying-from-a-workflow)."
10599+ },
1057010600 "get_environment": {
1057110601 "params": {
1057210602 "owner": "flagon-io",
+2−0
359359 &[("environment", "environment"), ("ref", "ref"), ("sha", "sha"), ("task", "task"), ("state", "state"), ("source", "source"), ("creator", "creator"), ("page", "page"), ("per_page", "per_page")],
360360 ),
361361 route("POST", "/repos/:owner/:name/deployments", Op::Deployments(DeploymentsOp::CreateDeployment), &[]),
362+ // A g1t Actions job opening a g1t.page build it runs itself.
363+ route("POST", "/repos/:owner/:name/deployments/builds", Op::Deployments(DeploymentsOp::StartDeploymentBuild), &[]),
362364 route("GET", "/repos/:owner/:name/deployments/:id", Op::Deployments(DeploymentsOp::GetDeployment), &[]),
363365 route("GET", "/repos/:owner/:name/deployments/:id/statuses", Op::Deployments(DeploymentsOp::ListDeploymentStatuses), &[]),
364366 route("POST", "/repos/:owner/:name/deployments/:id/statuses", Op::Deployments(DeploymentsOp::CreateDeploymentStatus), &[]),
+1−0
263263 a("create_deployment_status", Op::Deployments(DeploymentsOp::CreateDeploymentStatus), "Report where a deployment is: in_progress, success, failure"),
264264 a("list_environments", Op::Deployments(DeploymentsOp::ListEnvironments), "Environments with their current and latest deployments"),
265265 a("get_environment", Op::Deployments(DeploymentsOp::GetEnvironment), "One environment by name, with its protection rules"),
266+ a("start_deployment_build", Op::Deployments(DeploymentsOp::StartDeploymentBuild), "From a workflow job: open a g1t.page build it runs itself"),
266267 a("update_environment", Op::Protection(ProtectionOp::UpdateEnvironment), "Set an environment's reviewers, wait timer and branches"),
267268 a("delete_environment", Op::Protection(ProtectionOp::DeleteEnvironment), "Remove an environment's protection rules"),
268269 a("pending_deployments", Op::Protection(ProtectionOp::GetPendingDeployments), "The environments holding a run's jobs"),
+14−0
242242
243243 Actions that a composite action uses are downloaded when it runs.
244244
245+### g1t's own actions
246+
247+The `g1t` workspace on g1t.sh holds g1t's own actions, public, with a
248+`v1` tag that moves with each compatible release:
249+
250+| Action | What it does |
251+| --- | --- |
252+| [`g1t/deploy@v1`](https://g1t.sh/g1t/deploy) | Builds the checkout and puts it on [g1t.page](/guides/deployments/): production from the project's production branch, a preview from a pull request or another branch. The job needs `deployments: write`. See [deploying from a workflow](/guides/deployments/#deploying-from-a-workflow). |
253+| [`g1t/detect@v1`](https://g1t.sh/g1t/detect) | Says what that build would find (`kind`, `install`, `build`, `output-dir`) without building anything. |
254+
255+Both are composite actions around `g1t-runner`, which g1t's machines
256+have; on a [self-hosted runner](/guides/self-hosted-runners/) they use
257+the one on `PATH` or download the newest release.
258+
245259 ### Secrets for a called workflow
246260
247261 A called workflow gets only the secrets its caller passes, plus
+119−1
191191 The [checklist on the overview](/guides/projects/#get-to-production) also
192192 tracks the first production deploy, a custom domain and a first preview.
193193
194+## Deploying from a workflow
195+
196+A [workflow](/guides/actions/) job can build and deploy the project
197+itself, with g1t's own action, `g1t/deploy`. Use it when the build needs
198+steps of yours first (generated code, a test that must pass, a tool the
199+default build lacks), or to deploy from a branch without a pull request.
200+The app, its address, its checks and its custom domains are the same as
201+for the builds g1t starts by itself.
202+
203+```yaml
204+# .g1t/workflows/deploy.yml
205+name: Deploy
206+
207+on:
208+ push:
209+ branches: [main]
210+ pull_request:
211+
212+permissions:
213+ contents: read
214+ deployments: write
215+
216+jobs:
217+ deploy:
218+ runs-on: ubuntu-latest
219+ steps:
220+ - uses: actions/checkout@v5
221+ - uses: actions/setup-node@v4
222+ with:
223+ node-version: 24
224+ - run: npm ci
225+ - run: npm test
226+ - id: deploy
227+ uses: g1t/deploy@v1
228+ with:
229+ build-command: npm run build
230+ - run: echo "Live at ${{ steps.deploy.outputs.url }}"
231+```
232+
233+1. Turn Deployments on for the project, as in
234+ [Turn on deployments](#turn-on-deployments).
235+2. Add a workflow like the one above. The job needs `deployments: write`
236+ (or `pages: write`) in its `permissions:`.
237+3. Push. The run on `main` deploys production; a pull request's run
238+ deploys its preview.
239+
240+What the run builds:
241+
242+| The run | Builds |
243+| --- | --- |
244+| On the project's production branch (a push, `workflow_dispatch` or `schedule` there) | Production |
245+| For a pull request (`pull_request`, `pull_request_target`) | That pull request's preview, at its branch's address, or `pr-<number>` for one from a fork |
246+| On another branch | That branch's preview |
247+| For a tag | Nothing: the step fails and says to run the workflow on a branch |
248+
249+`g1t/deploy` takes these inputs:
250+
251+| Input | Default | |
252+| --- | --- | --- |
253+| `project` | The repository's one project with Deployments on | The project's slug. Needed when the repository has several. |
254+| `root-dir` | The project's **Root directory** | Where the project is in the checkout. |
255+| `build-command` | The project's **Build command**, else what the build finds | What builds it, run after the install. |
256+| `output-dir` | The project's **Output directory**, else the first folder found | The folder a static build writes. |
257+| `sha` | The run's commit | The commit the `g1t / deploy` check goes on. |
258+| `token` | `${{ github.token }}` | The job's own token. No other token can open a build. |
259+
260+Its outputs are `url` (the app's address), `deployment-id` (`dpl_…`) and
261+`kind` (`production` or `preview`).
262+
263+The action calls
264+[`POST /repos/{owner}/{repo}/deployments/builds`](/reference/api/deployments/start-deployment-build/),
265+which checks the plan and the spend limit as any build does, sets the
266+commit's `g1t / deploy` check to pending, and answers with a token for
267+that build alone and the project's
268+[variables and secrets](#secrets-and-variables) for production or
269+previews. The action masks the token and every secret in the log, then
270+runs `g1t-runner deploy` in the checkout, which installs, builds and
271+uploads exactly as [What deploys](#what-deploys) says; its output is in
272+the step's log and on the deployment's page. When the build fails, or
273+g1t does not take what it built, the step fails and the deployment says
274+why.
275+
276+- **Secrets.** A pull request from a fork or from someone without the
277+ Write role, and a run that had to be
278+ [approved](/guides/actions/#pull-requests), build without
279+ the project's secrets, as previews of them always have. Their token is
280+ read-only, so they cannot open a build at all unless the workflow runs
281+ on `pull_request_target`.
282+- **Cost.** The build runs in the job, so its time is the job's
283+ [Actions time](/guides/usage-and-billing/), free on a
284+ [self-hosted runner](/guides/self-hosted-runners/), and is not charged
285+ again as build seconds. Requests, CPU time and custom domains are
286+ charged as for any app.
287+- **g1t's own builds still run.** A push to the production branch and a
288+ pull request's push still start g1t's build too. Whichever starts
289+ later replaces the other, so the app ends up at the newest commit
290+ either way.
291+- **On your own machines**, the action uses the `g1t-runner` on `PATH`, or
292+ downloads the newest release and checks it against its `SHA256SUMS`. It
293+ runs on Linux and macOS, with `curl` and `jq` or `python3`.
294+
295+`g1t/detect` says what a build of the checkout would find, without
296+building anything: its outputs are `kind` (`workers`, `static`, `html` or
297+`none`), `install`, `build` and `output-dir`. Use them to cache, or to
298+fail early:
299+
300+```yaml
301+- id: detect
302+ uses: g1t/detect@v1
303+- if: steps.detect.outputs.kind == 'none'
304+ run: |
305+ echo "::error::Nothing here would be served: add an index.html or a build script."
306+ exit 1
307+```
308+
194309 ## Custom domains
195310
196311 Production can be served at a domain of your own, such as `example.com` or
352467 | | Costs g1t | You pay |
353468 | --- | --- | --- |
354469 | Projects, previews and the apps behind them | Next to nothing | Not charged |
355−| Builds, by the second, whether they succeed or fail | About $0.001 a minute | About $0.0012 a minute |
470+| Builds g1t runs, by the second, whether they succeed or fail (a [workflow's build](#deploying-from-a-workflow) is its job's Actions time instead) | About $0.001 a minute | About $0.0012 a minute |
356471 | Requests, to all of the workspace's apps | $0.30 a million | $0.36 a million |
357472 | CPU time your code spends computing (waiting on the network is not counted) | $0.02 a million ms | $0.024 a million ms |
358473 | Custom domains, with their certificates (a www/apex pair is two) | $0.10 a month | $0.12 a month, by the most the workspace had at once that month |
450565 | A domain stays at **Waiting for DNS** | Check the record against the one listed, remove other `A`/`AAAA` records for the same name, then choose **Check now**. |
451566 | A domain says "This domain is not set up" | It points at g1t, but no project has added it. Add it under **Settings → Domains**. |
452567 | "This pull request's commit no longer exists" | The pull request's head was force-pushed over, or its branch deleted, after the build was asked for. Push to the pull request again, or close it. g1t does not try that commit again. |
568+| g1t/deploy says "The job's token needs `deployments: write`" | Add `deployments: write` to the workflow's or the job's `permissions:`. A pull request's run from outside gets a read-only token and cannot deploy. |
569+| g1t/deploy says "Name the project to build" | The repository has several projects with Deployments on: give `project:`. |
570+| g1t/deploy says "g1t.page deploys branches and pull requests" | The run is for a tag. Run the workflow on a branch, such as on a push to `main`. |
453571 | "The build did not finish in 45 minutes" | The build stopped reporting: a build stops at 30 minutes, and one not heard from after 45 is failed. Make the build faster, or build less for previews with **Build command**. |
454572
455573 ## Running your own g1t
+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

This change is too large to show in full.