Skip to content

Commit

Merge branch 'claude/cool-newton-7nm8r9' (#36): real GitHub Actions on g1t, managed deploy workflows, g1t/deploy and the Sysbox runner 0.2.0. The merge keeps both sides' webhook events and API operations, says agent images need tar, guards the job engine's stop to Unix so the runner builds on Windows again, and keeps the moves tests' previews from going idle as the calendar passes their fixture's date.

syntaqxcommitted Parentsf5e317041c296aBrowse files
215 files+604−90/215 viewed
+81−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+# g1t's reusable workflows, in workflows/, go to g1t/workflows the same
9+# way (workflows/.g1t/workflows/deploy.yml is
10+# `g1t/workflows/.g1t/workflows/deploy.yml@v1`).
11+#
12+# Runs on a push to main that changes actions/ or workflows/, and by hand.
13+#
14+# Needs the repository secret G1T_ACTIONS_PUBLISH_TOKEN: an access token of
15+# the g1t workspace with contents: write on its action repositories
16+# (g1t/deploy, g1t/detect, and one for each new folder, made public
17+# before its first release). The job's own token reaches this repository
18+# only, so it cannot push there.
19+name: Actions release
20+
21+on:
22+ push:
23+ branches: [main]
24+ paths:
25+ - actions/**
26+ - workflows/**
27+ workflow_dispatch:
28+
29+permissions:
30+ contents: read
31+
32+concurrency:
33+ group: actions-release
34+ cancel-in-progress: false
35+
36+env:
37+ MAJOR: v1
38+ NAMESPACE: g1t
39+
40+jobs:
41+ publish:
42+ name: Publish g1t/<action>
43+ runs-on: ubuntu-latest
44+ timeout-minutes: 15
45+ steps:
46+ - uses: actions/checkout@v5
47+ with:
48+ fetch-depth: 0
49+ - name: Push each action and move its major tag
50+ env:
51+ PUBLISH_TOKEN: ${{ secrets.G1T_ACTIONS_PUBLISH_TOKEN }}
52+ SERVER: ${{ github.server_url }}
53+ run: |
54+ set -euo pipefail
55+ if [ -z "${PUBLISH_TOKEN}" ]; then
56+ echo "::error::Set the repository secret G1T_ACTIONS_PUBLISH_TOKEN (this workflow's header says what it needs)."
57+ exit 1
58+ fi
59+ echo "::add-mask::${PUBLISH_TOKEN}"
60+ auth="Authorization: Basic $(printf 'g1t:%s' "${PUBLISH_TOKEN}" | base64 -w0)"
61+ git config user.name g1t
62+ git config user.email g1t@users.noreply.g1t.sh
63+ for dir in actions/*/; do
64+ name="$(basename "${dir}")"
65+ [ -f "${dir}action.yml" ] || { echo "::warning::${dir} has no action.yml; skipped."; continue; }
66+ commit="$(git subtree split --prefix "actions/${name}" HEAD)"
67+ remote="${SERVER}/${NAMESPACE}/${name}.git"
68+ echo "Publishing actions/${name} (${commit}) to ${NAMESPACE}/${name}"
69+ git -c http.extraHeader="${auth}" push "${remote}" "${commit}:refs/heads/main"
70+ git -c http.extraHeader="${auth}" push --force "${remote}" "${commit}:refs/tags/${MAJOR}"
71+ echo "- \`${NAMESPACE}/${name}@${MAJOR}\` is ${commit}" >> "${GITHUB_STEP_SUMMARY}"
72+ done
73+ # g1t's reusable workflows, from workflows/, as g1t/workflows.
74+ if [ -d workflows/.g1t/workflows ]; then
75+ commit="$(git subtree split --prefix workflows HEAD)"
76+ remote="${SERVER}/${NAMESPACE}/workflows.git"
77+ echo "Publishing workflows (${commit}) to ${NAMESPACE}/workflows"
78+ git -c http.extraHeader="${auth}" push "${remote}" "${commit}:refs/heads/main"
79+ git -c http.extraHeader="${auth}" push --force "${remote}" "${commit}:refs/tags/${MAJOR}"
80+ echo "- \`${NAMESPACE}/workflows@${MAJOR}\` is ${commit}" >> "${GITHUB_STEP_SUMMARY}"
81+ fi
+2−2
6868 # The larger machine: linking Tauri on the small one dies with a bus error.
6969 runs-on: g1t-4core
7070 environment: production
71− timeout-minutes: 90
71+ timeout-minutes: 60 # the most g1t's own machines run a job
7272 steps:
7373 - uses: actions/checkout@v5
7474 - name: What Tauri needs on Linux
139139 if: vars.DESKTOP_WINDOWS_FROM_LINUX == 'true' && vars.DESKTOP_WINDOWS_RUNNER != 'true'
140140 runs-on: g1t-4core
141141 environment: production
142− timeout-minutes: 90
142+ timeout-minutes: 60 # the most g1t's own machines run a job
143143 steps:
144144 - uses: actions/checkout@v5
145145 - name: What the cross build needs
+2−1
10961096
10971097 [[package]]
10981098 name = "g1t-runner"
1099−version = "0.1.3"
1099+version = "0.2.0"
11001100 dependencies = [
11011101 "anyhow",
11021102 "base64 0.22.1",
11071107 "g1t-scan",
11081108 "hex",
11091109 "libc",
1110+ "regex",
11101111 "serde",
11111112 "serde_json",
11121113 "serde_yaml",
+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]
+6−2
8383
8484 pub fn description(self) -> &'static str {
8585 match self {
86− MirrorsOp::GetMirror => "A repository's links to other hosts. `remotes` lists each with its `role` (`leader`: the remote leads and this repository is its mirror; `follower`: g1t leads and the remote is kept in step), `provider` (`github`, `g1t` or `git`), `name` (`github.com/acme/web`), `state` (a mirror is `standby`, `ci`, `takeover` or `handing_back`; a follower is `following` or `stuck`), `reachable` and `unreachable_since`, `synced_at`, `last_error` and its `settings`. `can_manage` says whether you may change them. A mirror standing by is read-only on g1t and runs nothing. Needs read access.",
86+ MirrorsOp::GetMirror => "A repository's links to other hosts. `remotes` lists each with its `role` (`leader`: the remote leads and this repository is its mirror; `follower`: g1t leads and the remote is kept in step), `provider` (`github`, `g1t` or `git`), `name` (`github.com/acme/web`), `state` (a mirror is `standby`, `ci`, `takeover` or `handing_back`; a follower is `following` or `stuck`), `reachable` and `unreachable_since`, `synced_at`, `last_error`, `checks_problem` (why g1t could not report checks to the remote, such as the GitHub App lacking the Commit statuses permission; null once a report goes through) and its `settings`. `can_manage` says whether you may change them. A mirror standing by is read-only on g1t and runs nothing. Needs read access.",
8787 MirrorsOp::GetHandBackPlan => "What handing a takeover back would do, ref by ref, in `plan.refs`: each with `base` (where it was when the takeover began), `ours`, `theirs` and `action`: `push` (only g1t moved), `fetch` (only the remote moved), `pull_request` (the remote protects the branch: g1t's commits go to `g1t/handback/<branch>` there as a pull request, and g1t follows the remote's branch) or `diverged` (both moved: it needs a decision). `plan.reachable` is false while the remote does not answer; `plan.ready` once it answers and every diverged ref has a decision. Asks the remote, so it takes a moment. Needs the Admin role.",
8888 MirrorsOp::TakeOver => "Take over a mirror: g1t leads it for now, so pushes, pull requests, issues, agents and workflows work on g1t, whether or not the remote is answering. Each ref's commit is recorded as where the takeover began. Workflows that deploy wait for approval, unless the link's settings say otherwise. End it with hand_back_mirror, or keep it with move_mirror_to_g1t. Needs the Admin role.",
8989 MirrorsOp::SetCiFailover => "Start (`on: true`) or end (`on: false`) CI failover on a mirror standing by: the remote keeps the code, and g1t runs its workflows, `.github/workflows` as well as `.g1t/workflows`, on each push copied in. Workflows that deploy wait for approval unless the link's settings say otherwise. Needs the Admin role.",
9191 MirrorsOp::MoveToG1t => "Move a mirror to g1t for good: it stops being a mirror and g1t no longer tracks the remote, so pushes made there no longer come here. Allowed while it stands by, in CI failover, or during a takeover, whose work stays as it is. With `keep_remote_updated: true` the remote becomes a follower instead of being unlinked, and g1t pushes to it from then on. Needs the Admin role; agents' tokens are refused.",
9292 MirrorsOp::SyncMirror => "Bring a repository's remotes in step now: a mirror standing by or in CI failover copies the remote's branches and tags in; each follower is pushed g1t's (which also clears a follower that was stuck by pushing over what changed there). Needs push access.",
9393 MirrorsOp::AddRemote => "Link a repository to a remote on another g1t or any git host over HTTPS (GitHub repositories are linked by importing them through g1t's GitHub App). `role` `follower`: g1t leads and pushes to it. `role` `leader`: this repository becomes its mirror; only an empty repository can, and it is filled from the remote. `url` is its https clone address, `token` a token that can read and push (kept sealed, never returned), `username` the user it is sent as. Needs the Admin role; agents' tokens are refused.",
94− MirrorsOp::UpdateRemote => "Change a remote's `settings`, all optional: `notify` (`banner`, or `inbox` to also tell the workspace's owners), `take_over_after` (minutes, 5 to 1440, after which g1t takes over on its own while the remote does not answer; null for never), `hand_back` (`when_clean`: an automatic takeover goes back on its own once every ref goes back without a decision; `ask`), `keep_ci_warm` (run `.g1t/workflows` on pushes copied in while standing by), `github_workflows` (run `.github/workflows` in CI failover and takeovers), `hold_deploys`, and for a follower `remote_pushes` (`adopt` fast-forwards made there, or `overwrite` them). Needs the Admin role; agents' tokens are refused.",
94+ MirrorsOp::UpdateRemote => "Change a remote's `settings`, all optional: `notify` (`banner`, or `inbox` to also tell the workspace's owners), `take_over_after` (minutes, 5 to 1440, after which g1t takes over on its own while the remote does not answer; null for never), `hand_back` (`when_clean`: an automatic takeover goes back on its own once every ref goes back without a decision; `ask`), `keep_ci_warm` (run `.g1t/workflows` on pushes copied in while standing by), `github_workflows` (run `.github/workflows` in CI failover and takeovers), `hold_deploys`, `report_checks` (a GitHub mirror: report the statuses g1t sets on a commit, such as workflow runs and deploys, to the commit on GitHub as `g1t / <context>` while g1t runs its workflows; on by default), and for a follower `remote_pushes` (`adopt` fast-forwards made there, or `overwrite` them). Needs the Admin role; agents' tokens are refused.",
9595 MirrorsOp::RemoveRemote => "Unlink a remote. A mirror becomes an ordinary repository with what it has; a follower is no longer pushed to. Refused during a takeover: hand it back or move it to g1t first. Needs the Admin role; agents' tokens are refused.",
9696 }
9797 }
146146 "keep_ci_warm": { "type": "boolean" },
147147 "github_workflows": { "type": "boolean" },
148148 "hold_deploys": { "type": "boolean" },
149+ "report_checks": { "type": "boolean" },
149150 "remote_pushes": { "type": "string", "enum": ["adopt", "overwrite"] },
150151 }),
151152 &["repo", "id"],
195196 ("keep_ci_warm", &mut settings.keep_ci_warm),
196197 ("github_workflows", &mut settings.github_workflows),
197198 ("hold_deploys", &mut settings.hold_deploys),
199+ ("report_checks", &mut settings.report_checks),
198200 ] {
199201 if let Some(value) = input.get(key).filter(|value| !value.is_null()) {
200202 *field = flag(value).ok_or_else(|| format!("{key} is true or false."))?;
355357 assert!(changed.keep_ci_warm);
356358 assert_eq!(changed.hand_back, HandBack::WhenClean);
357359 assert_eq!(changed.remote_pushes, RemotePushes::Adopt);
360+ assert!(changed.report_checks, "checks go to GitHub unless turned off");
361+ assert!(!settings_from(base.clone(), &json!({ "report_checks": false })).unwrap().report_checks);
358362 let cleared = settings_from(changed, &json!({ "take_over_after": null })).unwrap();
359363 assert_eq!(cleared.take_over_after, None);
360364 assert!(settings_from(base.clone(), &json!({ "notify": "pager" })).is_err());
+1−0
475475 Op::Deployments(DeploymentsOp::CreateDeploymentStatus),
476476 Op::Deployments(DeploymentsOp::ListEnvironments),
477477 Op::Deployments(DeploymentsOp::GetEnvironment),
478+ Op::Deployments(DeploymentsOp::StartDeploymentBuild),
478479 ],
479480 ),
480481 (
+3−2
711711 }
712712
713713 impl Op {
714− pub const ALL: [Op; 379] = [
714+ pub const ALL: [Op; 380] = [
715715 Op::Whoami,
716716 Op::GetWorkspace,
717717 Op::CreateWorkspace,
995995 Op::Deployments(DeploymentsOp::CreateDeploymentStatus),
996996 Op::Deployments(DeploymentsOp::ListEnvironments),
997997 Op::Deployments(DeploymentsOp::GetEnvironment),
998+ Op::Deployments(DeploymentsOp::StartDeploymentBuild),
998999 Op::Artifacts(ArtifactsOp::ListArtifacts),
9991000 Op::Artifacts(ArtifactsOp::ListRunArtifacts),
10001001 Op::Artifacts(ArtifactsOp::GetArtifact),
27642765 "repo": repo_schema(),
27652766 "workflow": { "type": "string", "description": "The workflow's id or file name." },
27662767 "ref": { "type": "string", "description": "A branch or tag. The default branch if not given." },
2767− "inputs": { "type": "object", "description": "The workflow_dispatch inputs, by name." },
2768+ "inputs": { "type": "object", "description": "The workflow_dispatch inputs, by name. Only inputs the workflow declares: any other is refused. A boolean is true or false, a number a number, a choice one of its options, and an environment one of the repository's." },
27682769 }),
27692770 &["repo", "workflow"],
27702771 ),
+46−0
1184511845 },
1184611846 "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."
1184711847 },
11848+ "start_deployment_build": {
11849+ "params": {
11850+ "owner": "flagon-io",
11851+ "name": "hello"
11852+ },
11853+ "request": {
11854+ "project": "hello"
11855+ },
11856+ "response": {
11857+ "deploy_id": "dpl_01kq9c4e6g8j0m2p4r6t8v0x2z",
11858+ "token": "…",
11859+ "kind": "preview",
11860+ "project": "hello",
11861+ "branch": "new-header",
11862+ "number": 42,
11863+ "commit": "9f2c4e6a8b0d1f3e5a7c9b1d3f5e7a9c0b2d4f6e",
11864+ "url": "https://hello-git-new-header-flagonio.g1t.page",
11865+ "trusted": true,
11866+ "root_dir": "",
11867+ "build_command": null,
11868+ "output_dir": null,
11869+ "build_env": {
11870+ "PUBLIC_API_URL": "https://api.example.com"
11871+ },
11872+ "build_secrets": {
11873+ "SENTRY_AUTH_TOKEN": "…"
11874+ }
11875+ },
11876+ "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)."
11877+ },
1184811878 "get_environment": {
1184911879 "params": {
1185011880 "owner": "flagon-io",
1370013730 "unreachable_since": null,
1370113731 "synced_at": "2026-10-08T13:57:42.000Z",
1370213732 "last_error": null,
13733+ "checks_problem": null,
1370313734 "settings": {
1370413735 "notify": "banner",
1370513736 "take_over_after": null,
1370713738 "keep_ci_warm": false,
1370813739 "github_workflows": true,
1370913740 "hold_deploys": true,
13741+ "report_checks": true,
1371013742 "remote_pushes": "adopt"
1371113743 },
1371213744 "created_at": "2026-10-05T10:00:00.000Z"
1373913771 "unreachable_since": null,
1374013772 "synced_at": "2026-10-08T13:57:42.000Z",
1374113773 "last_error": null,
13774+ "checks_problem": null,
1374213775 "settings": {
1374313776 "notify": "banner",
1374413777 "take_over_after": null,
1374613779 "keep_ci_warm": false,
1374713780 "github_workflows": true,
1374813781 "hold_deploys": true,
13782+ "report_checks": true,
1374913783 "remote_pushes": "adopt"
1375013784 },
1375113785 "created_at": "2026-10-05T10:00:00.000Z"
1380713841 "unreachable_since": "2026-10-08T14:04:00.000Z",
1380813842 "synced_at": "2026-10-08T13:57:42.000Z",
1380913843 "last_error": null,
13844+ "checks_problem": null,
1381013845 "settings": {
1381113846 "notify": "banner",
1381213847 "take_over_after": null,
1381413849 "keep_ci_warm": false,
1381513850 "github_workflows": true,
1381613851 "hold_deploys": true,
13852+ "report_checks": true,
1381713853 "remote_pushes": "adopt"
1381813854 },
1381913855 "created_at": "2026-10-05T10:00:00.000Z"
1384813884 "unreachable_since": null,
1384913885 "synced_at": "2026-10-08T13:57:42.000Z",
1385013886 "last_error": null,
13887+ "checks_problem": null,
1385113888 "settings": {
1385213889 "notify": "banner",
1385313890 "take_over_after": null,
1385513892 "keep_ci_warm": false,
1385613893 "github_workflows": true,
1385713894 "hold_deploys": true,
13895+ "report_checks": true,
1385813896 "remote_pushes": "adopt"
1385913897 },
1386013898 "created_at": "2026-10-05T10:00:00.000Z"
1389113929 "unreachable_since": null,
1389213930 "synced_at": "2026-10-08T13:57:42.000Z",
1389313931 "last_error": null,
13932+ "checks_problem": null,
1389413933 "settings": {
1389513934 "notify": "banner",
1389613935 "take_over_after": null,
1389813937 "keep_ci_warm": false,
1389913938 "github_workflows": true,
1390013939 "hold_deploys": true,
13940+ "report_checks": true,
1390113941 "remote_pushes": "adopt"
1390213942 },
1390313943 "created_at": "2026-10-05T10:00:00.000Z"
1394913989 "unreachable_since": null,
1395013990 "synced_at": "2026-10-08T13:57:42.000Z",
1395113991 "last_error": null,
13992+ "checks_problem": null,
1395213993 "settings": {
1395313994 "notify": "banner",
1395413995 "take_over_after": null,
1395613997 "keep_ci_warm": false,
1395713998 "github_workflows": true,
1395813999 "hold_deploys": true,
14000+ "report_checks": true,
1395914001 "remote_pushes": "adopt"
1396014002 },
1396114003 "created_at": "2026-10-05T10:00:00.000Z"
1399214034 "unreachable_since": null,
1399314035 "synced_at": "2026-10-08T13:57:42.000Z",
1399414036 "last_error": null,
14037+ "checks_problem": null,
1399514038 "settings": {
1399614039 "notify": "banner",
1399714040 "take_over_after": null,
1399914042 "keep_ci_warm": false,
1400014043 "github_workflows": true,
1400114044 "hold_deploys": true,
14045+ "report_checks": true,
1400214046 "remote_pushes": "adopt"
1400314047 },
1400414048 "created_at": "2026-10-05T10:00:00.000Z"
1403014074 "unreachable_since": null,
1403114075 "synced_at": "2026-10-08T13:57:42.000Z",
1403214076 "last_error": null,
14077+ "checks_problem": null,
1403314078 "settings": {
1403414079 "notify": "inbox",
1403514080 "take_over_after": 30,
1403714082 "keep_ci_warm": false,
1403814083 "github_workflows": true,
1403914084 "hold_deploys": true,
14085+ "report_checks": true,
1404014086 "remote_pushes": "adopt"
1404114087 },
1404214088 "created_at": "2026-10-05T10:00:00.000Z"
+2−0
369369 &[("environment", "environment"), ("ref", "ref"), ("sha", "sha"), ("task", "task"), ("state", "state"), ("source", "source"), ("creator", "creator"), ("page", "page"), ("per_page", "per_page")],
370370 ),
371371 route("POST", "/repos/:owner/:name/deployments", Op::Deployments(DeploymentsOp::CreateDeployment), &[]),
372+ // A g1t Actions job opening a g1t.page build it runs itself.
373+ route("POST", "/repos/:owner/:name/deployments/builds", Op::Deployments(DeploymentsOp::StartDeploymentBuild), &[]),
372374 route("GET", "/repos/:owner/:name/deployments/:id", Op::Deployments(DeploymentsOp::GetDeployment), &[]),
373375 route("GET", "/repos/:owner/:name/deployments/:id/statuses", Op::Deployments(DeploymentsOp::ListDeploymentStatuses), &[]),
374376 route("POST", "/repos/:owner/:name/deployments/:id/statuses", Op::Deployments(DeploymentsOp::CreateDeploymentStatus), &[]),
+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.

+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.

+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.

+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.

+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.

+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.

+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.

+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.

+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.

+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.

+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.

+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.

+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.

+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.

+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.