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.
| 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 |
| 68 | 68 | # The larger machine: linking Tauri on the small one dies with a bus error. | |
| 69 | 69 | runs-on: g1t-4core | |
| 70 | 70 | environment: production | |
| 71 | − | timeout-minutes: 90 | |
| 71 | + | timeout-minutes: 60 # the most g1t's own machines run a job | |
| 72 | 72 | steps: | |
| 73 | 73 | - uses: actions/checkout@v5 | |
| 74 | 74 | - name: What Tauri needs on Linux | |
| ⋯ | |||
| 139 | 139 | if: vars.DESKTOP_WINDOWS_FROM_LINUX == 'true' && vars.DESKTOP_WINDOWS_RUNNER != 'true' | |
| 140 | 140 | runs-on: g1t-4core | |
| 141 | 141 | environment: production | |
| 142 | − | timeout-minutes: 90 | |
| 142 | + | timeout-minutes: 60 # the most g1t's own machines run a job | |
| 143 | 143 | steps: | |
| 144 | 144 | - uses: actions/checkout@v5 | |
| 145 | 145 | - name: What the cross build needs | |
| 1096 | 1096 | ||
| 1097 | 1097 | [[package]] | |
| 1098 | 1098 | name = "g1t-runner" | |
| 1099 | − | version = "0.1.3" | |
| 1099 | + | version = "0.2.0" | |
| 1100 | 1100 | dependencies = [ | |
| 1101 | 1101 | "anyhow", | |
| 1102 | 1102 | "base64 0.22.1", | |
| ⋯ | |||
| 1107 | 1107 | "g1t-scan", | |
| 1108 | 1108 | "hex", | |
| 1109 | 1109 | "libc", | |
| 1110 | + | "regex", | |
| 1110 | 1111 | "serde", | |
| 1111 | 1112 | "serde_json", | |
| 1112 | 1113 | "serde_yaml", | |
| 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`. |
| 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" |
| 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 |
| 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" |
| 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. |
| 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" |
| 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" |
| 24 | 24 | CreateDeploymentStatus, | |
| 25 | 25 | ListEnvironments, | |
| 26 | 26 | GetEnvironment, | |
| 27 | + | StartDeploymentBuild, | |
| 27 | 28 | } | |
| 28 | 29 | ||
| 29 | 30 | /// The states a deployment's status can have. | |
| ⋯ | |||
| 33 | 34 | /// Every one: `Op::ALL` lists each as `Op::Deployments(…)`, which a | |
| 34 | 35 | /// test checks against this. | |
| 35 | 36 | #[cfg(test)] | |
| 36 | − | pub const ALL: [DeploymentsOp; 7] = [ | |
| 37 | + | pub const ALL: [DeploymentsOp; 8] = [ | |
| 37 | 38 | DeploymentsOp::ListDeployments, | |
| 38 | 39 | DeploymentsOp::GetDeployment, | |
| 39 | 40 | DeploymentsOp::CreateDeployment, | |
| ⋯ | |||
| 41 | 42 | DeploymentsOp::CreateDeploymentStatus, | |
| 42 | 43 | DeploymentsOp::ListEnvironments, | |
| 43 | 44 | DeploymentsOp::GetEnvironment, | |
| 45 | + | DeploymentsOp::StartDeploymentBuild, | |
| 44 | 46 | ]; | |
| 45 | 47 | ||
| 46 | 48 | pub fn name(self) -> &'static str { | |
| ⋯ | |||
| 52 | 54 | DeploymentsOp::CreateDeploymentStatus => "create_deployment_status", | |
| 53 | 55 | DeploymentsOp::ListEnvironments => "list_environments", | |
| 54 | 56 | DeploymentsOp::GetEnvironment => "get_environment", | |
| 57 | + | DeploymentsOp::StartDeploymentBuild => "start_deployment_build", | |
| 55 | 58 | } | |
| 56 | 59 | } | |
| 57 | 60 | ||
| ⋯ | |||
| 65 | 68 | DeploymentsOp::CreateDeploymentStatus => "Create a deployment status", | |
| 66 | 69 | DeploymentsOp::ListEnvironments => "List environments", | |
| 67 | 70 | DeploymentsOp::GetEnvironment => "Get an environment", | |
| 71 | + | DeploymentsOp::StartDeploymentBuild => "Start a g1t.page build from a workflow", | |
| 68 | 72 | } | |
| 69 | 73 | } | |
| 70 | 74 | ||
| ⋯ | |||
| 77 | 81 | 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.", | |
| 78 | 82 | 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.", | |
| 79 | 83 | 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.", | |
| 80 | 85 | } | |
| 81 | 86 | } | |
| 82 | 87 | ||
| 83 | 88 | /// Whether it changes anything. | |
| 84 | 89 | pub fn writes(self) -> bool { | |
| 85 | − | matches!(self, DeploymentsOp::CreateDeployment | DeploymentsOp::CreateDeploymentStatus) | |
| 90 | + | matches!( | |
| 91 | + | self, | |
| 92 | + | DeploymentsOp::CreateDeployment | DeploymentsOp::CreateDeploymentStatus | DeploymentsOp::StartDeploymentBuild | |
| 93 | + | ) | |
| 86 | 94 | } | |
| 87 | 95 | ||
| 88 | 96 | pub fn input(self) -> Value { | |
| ⋯ | |||
| 137 | 145 | &["repo", "id", "state"], | |
| 138 | 146 | ), | |
| 139 | 147 | 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 | + | ), | |
| 140 | 156 | DeploymentsOp::GetEnvironment => ( | |
| 141 | 157 | json!({ | |
| 142 | 158 | "repo": repo, | |
| ⋯ | |||
| 236 | 252 | "create_deployment_status" | |
| 237 | 253 | } | |
| 238 | 254 | 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 | + | } | |
| 239 | 265 | DeploymentsOp::GetEnvironment => { | |
| 240 | 266 | let name = input["environment"].as_str().map(str::trim).filter(|name| !name.is_empty()).ok_or("Name the environment.")?; | |
| 241 | 267 | out.insert("name".into(), name.into()); | |
| ⋯ | |||
| 294 | 320 | assert!(super::args(DeploymentsOp::CreateDeploymentStatus, &json!({ "repo": "acme/web", "id": "dep_1" })).is_err()); | |
| 295 | 321 | let (method, args) = super::args(DeploymentsOp::GetEnvironment, &json!({ "repo": "acme/web", "environment": "review/x" })).unwrap(); | |
| 296 | 322 | 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()); | |
| 297 | 328 | } | |
| 298 | 329 | ||
| 299 | 330 | #[test] | |
| 83 | 83 | ||
| 84 | 84 | pub fn description(self) -> &'static str { | |
| 85 | 85 | 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.", | |
| 87 | 87 | 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.", | |
| 88 | 88 | 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.", | |
| 89 | 89 | 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.", | |
| ⋯ | |||
| 91 | 91 | 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.", | |
| 92 | 92 | 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.", | |
| 93 | 93 | 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.", | |
| 95 | 95 | 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.", | |
| 96 | 96 | } | |
| 97 | 97 | } | |
| ⋯ | |||
| 146 | 146 | "keep_ci_warm": { "type": "boolean" }, | |
| 147 | 147 | "github_workflows": { "type": "boolean" }, | |
| 148 | 148 | "hold_deploys": { "type": "boolean" }, | |
| 149 | + | "report_checks": { "type": "boolean" }, | |
| 149 | 150 | "remote_pushes": { "type": "string", "enum": ["adopt", "overwrite"] }, | |
| 150 | 151 | }), | |
| 151 | 152 | &["repo", "id"], | |
| ⋯ | |||
| 195 | 196 | ("keep_ci_warm", &mut settings.keep_ci_warm), | |
| 196 | 197 | ("github_workflows", &mut settings.github_workflows), | |
| 197 | 198 | ("hold_deploys", &mut settings.hold_deploys), | |
| 199 | + | ("report_checks", &mut settings.report_checks), | |
| 198 | 200 | ] { | |
| 199 | 201 | if let Some(value) = input.get(key).filter(|value| !value.is_null()) { | |
| 200 | 202 | *field = flag(value).ok_or_else(|| format!("{key} is true or false."))?; | |
| ⋯ | |||
| 355 | 357 | assert!(changed.keep_ci_warm); | |
| 356 | 358 | assert_eq!(changed.hand_back, HandBack::WhenClean); | |
| 357 | 359 | 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); | |
| 358 | 362 | let cleared = settings_from(changed, &json!({ "take_over_after": null })).unwrap(); | |
| 359 | 363 | assert_eq!(cleared.take_over_after, None); | |
| 360 | 364 | assert!(settings_from(base.clone(), &json!({ "notify": "pager" })).is_err()); | |
| 475 | 475 | Op::Deployments(DeploymentsOp::CreateDeploymentStatus), | |
| 476 | 476 | Op::Deployments(DeploymentsOp::ListEnvironments), | |
| 477 | 477 | Op::Deployments(DeploymentsOp::GetEnvironment), | |
| 478 | + | Op::Deployments(DeploymentsOp::StartDeploymentBuild), | |
| 478 | 479 | ], | |
| 479 | 480 | ), | |
| 480 | 481 | ( |
| 711 | 711 | } | |
| 712 | 712 | ||
| 713 | 713 | impl Op { | |
| 714 | − | pub const ALL: [Op; 379] = [ | |
| 714 | + | pub const ALL: [Op; 380] = [ | |
| 715 | 715 | Op::Whoami, | |
| 716 | 716 | Op::GetWorkspace, | |
| 717 | 717 | Op::CreateWorkspace, | |
| ⋯ | |||
| 995 | 995 | Op::Deployments(DeploymentsOp::CreateDeploymentStatus), | |
| 996 | 996 | Op::Deployments(DeploymentsOp::ListEnvironments), | |
| 997 | 997 | Op::Deployments(DeploymentsOp::GetEnvironment), | |
| 998 | + | Op::Deployments(DeploymentsOp::StartDeploymentBuild), | |
| 998 | 999 | Op::Artifacts(ArtifactsOp::ListArtifacts), | |
| 999 | 1000 | Op::Artifacts(ArtifactsOp::ListRunArtifacts), | |
| 1000 | 1001 | Op::Artifacts(ArtifactsOp::GetArtifact), | |
| ⋯ | |||
| 2764 | 2765 | "repo": repo_schema(), | |
| 2765 | 2766 | "workflow": { "type": "string", "description": "The workflow's id or file name." }, | |
| 2766 | 2767 | "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." }, | |
| 2768 | 2769 | }), | |
| 2769 | 2770 | &["repo", "workflow"], | |
| 2770 | 2771 | ), | |
| 11845 | 11845 | }, | |
| 11846 | 11846 | "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." | |
| 11847 | 11847 | }, | |
| 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 | + | }, | |
| 11848 | 11878 | "get_environment": { | |
| 11849 | 11879 | "params": { | |
| 11850 | 11880 | "owner": "flagon-io", | |
| ⋯ | |||
| 13700 | 13730 | "unreachable_since": null, | |
| 13701 | 13731 | "synced_at": "2026-10-08T13:57:42.000Z", | |
| 13702 | 13732 | "last_error": null, | |
| 13733 | + | "checks_problem": null, | |
| 13703 | 13734 | "settings": { | |
| 13704 | 13735 | "notify": "banner", | |
| 13705 | 13736 | "take_over_after": null, | |
| ⋯ | |||
| 13707 | 13738 | "keep_ci_warm": false, | |
| 13708 | 13739 | "github_workflows": true, | |
| 13709 | 13740 | "hold_deploys": true, | |
| 13741 | + | "report_checks": true, | |
| 13710 | 13742 | "remote_pushes": "adopt" | |
| 13711 | 13743 | }, | |
| 13712 | 13744 | "created_at": "2026-10-05T10:00:00.000Z" | |
| ⋯ | |||
| 13739 | 13771 | "unreachable_since": null, | |
| 13740 | 13772 | "synced_at": "2026-10-08T13:57:42.000Z", | |
| 13741 | 13773 | "last_error": null, | |
| 13774 | + | "checks_problem": null, | |
| 13742 | 13775 | "settings": { | |
| 13743 | 13776 | "notify": "banner", | |
| 13744 | 13777 | "take_over_after": null, | |
| ⋯ | |||
| 13746 | 13779 | "keep_ci_warm": false, | |
| 13747 | 13780 | "github_workflows": true, | |
| 13748 | 13781 | "hold_deploys": true, | |
| 13782 | + | "report_checks": true, | |
| 13749 | 13783 | "remote_pushes": "adopt" | |
| 13750 | 13784 | }, | |
| 13751 | 13785 | "created_at": "2026-10-05T10:00:00.000Z" | |
| ⋯ | |||
| 13807 | 13841 | "unreachable_since": "2026-10-08T14:04:00.000Z", | |
| 13808 | 13842 | "synced_at": "2026-10-08T13:57:42.000Z", | |
| 13809 | 13843 | "last_error": null, | |
| 13844 | + | "checks_problem": null, | |
| 13810 | 13845 | "settings": { | |
| 13811 | 13846 | "notify": "banner", | |
| 13812 | 13847 | "take_over_after": null, | |
| ⋯ | |||
| 13814 | 13849 | "keep_ci_warm": false, | |
| 13815 | 13850 | "github_workflows": true, | |
| 13816 | 13851 | "hold_deploys": true, | |
| 13852 | + | "report_checks": true, | |
| 13817 | 13853 | "remote_pushes": "adopt" | |
| 13818 | 13854 | }, | |
| 13819 | 13855 | "created_at": "2026-10-05T10:00:00.000Z" | |
| ⋯ | |||
| 13848 | 13884 | "unreachable_since": null, | |
| 13849 | 13885 | "synced_at": "2026-10-08T13:57:42.000Z", | |
| 13850 | 13886 | "last_error": null, | |
| 13887 | + | "checks_problem": null, | |
| 13851 | 13888 | "settings": { | |
| 13852 | 13889 | "notify": "banner", | |
| 13853 | 13890 | "take_over_after": null, | |
| ⋯ | |||
| 13855 | 13892 | "keep_ci_warm": false, | |
| 13856 | 13893 | "github_workflows": true, | |
| 13857 | 13894 | "hold_deploys": true, | |
| 13895 | + | "report_checks": true, | |
| 13858 | 13896 | "remote_pushes": "adopt" | |
| 13859 | 13897 | }, | |
| 13860 | 13898 | "created_at": "2026-10-05T10:00:00.000Z" | |
| ⋯ | |||
| 13891 | 13929 | "unreachable_since": null, | |
| 13892 | 13930 | "synced_at": "2026-10-08T13:57:42.000Z", | |
| 13893 | 13931 | "last_error": null, | |
| 13932 | + | "checks_problem": null, | |
| 13894 | 13933 | "settings": { | |
| 13895 | 13934 | "notify": "banner", | |
| 13896 | 13935 | "take_over_after": null, | |
| ⋯ | |||
| 13898 | 13937 | "keep_ci_warm": false, | |
| 13899 | 13938 | "github_workflows": true, | |
| 13900 | 13939 | "hold_deploys": true, | |
| 13940 | + | "report_checks": true, | |
| 13901 | 13941 | "remote_pushes": "adopt" | |
| 13902 | 13942 | }, | |
| 13903 | 13943 | "created_at": "2026-10-05T10:00:00.000Z" | |
| ⋯ | |||
| 13949 | 13989 | "unreachable_since": null, | |
| 13950 | 13990 | "synced_at": "2026-10-08T13:57:42.000Z", | |
| 13951 | 13991 | "last_error": null, | |
| 13992 | + | "checks_problem": null, | |
| 13952 | 13993 | "settings": { | |
| 13953 | 13994 | "notify": "banner", | |
| 13954 | 13995 | "take_over_after": null, | |
| ⋯ | |||
| 13956 | 13997 | "keep_ci_warm": false, | |
| 13957 | 13998 | "github_workflows": true, | |
| 13958 | 13999 | "hold_deploys": true, | |
| 14000 | + | "report_checks": true, | |
| 13959 | 14001 | "remote_pushes": "adopt" | |
| 13960 | 14002 | }, | |
| 13961 | 14003 | "created_at": "2026-10-05T10:00:00.000Z" | |
| ⋯ | |||
| 13992 | 14034 | "unreachable_since": null, | |
| 13993 | 14035 | "synced_at": "2026-10-08T13:57:42.000Z", | |
| 13994 | 14036 | "last_error": null, | |
| 14037 | + | "checks_problem": null, | |
| 13995 | 14038 | "settings": { | |
| 13996 | 14039 | "notify": "banner", | |
| 13997 | 14040 | "take_over_after": null, | |
| ⋯ | |||
| 13999 | 14042 | "keep_ci_warm": false, | |
| 14000 | 14043 | "github_workflows": true, | |
| 14001 | 14044 | "hold_deploys": true, | |
| 14045 | + | "report_checks": true, | |
| 14002 | 14046 | "remote_pushes": "adopt" | |
| 14003 | 14047 | }, | |
| 14004 | 14048 | "created_at": "2026-10-05T10:00:00.000Z" | |
| ⋯ | |||
| 14030 | 14074 | "unreachable_since": null, | |
| 14031 | 14075 | "synced_at": "2026-10-08T13:57:42.000Z", | |
| 14032 | 14076 | "last_error": null, | |
| 14077 | + | "checks_problem": null, | |
| 14033 | 14078 | "settings": { | |
| 14034 | 14079 | "notify": "inbox", | |
| 14035 | 14080 | "take_over_after": 30, | |
| ⋯ | |||
| 14037 | 14082 | "keep_ci_warm": false, | |
| 14038 | 14083 | "github_workflows": true, | |
| 14039 | 14084 | "hold_deploys": true, | |
| 14085 | + | "report_checks": true, | |
| 14040 | 14086 | "remote_pushes": "adopt" | |
| 14041 | 14087 | }, | |
| 14042 | 14088 | "created_at": "2026-10-05T10:00:00.000Z" | |
| 369 | 369 | &[("environment", "environment"), ("ref", "ref"), ("sha", "sha"), ("task", "task"), ("state", "state"), ("source", "source"), ("creator", "creator"), ("page", "page"), ("per_page", "per_page")], | |
| 370 | 370 | ), | |
| 371 | 371 | 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), &[]), | |
| 372 | 374 | route("GET", "/repos/:owner/:name/deployments/:id", Op::Deployments(DeploymentsOp::GetDeployment), &[]), | |
| 373 | 375 | route("GET", "/repos/:owner/:name/deployments/:id/statuses", Op::Deployments(DeploymentsOp::ListDeploymentStatuses), &[]), | |
| 374 | 376 | route("POST", "/repos/:owner/:name/deployments/:id/statuses", Op::Deployments(DeploymentsOp::CreateDeploymentStatus), &[]), |
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
This change is too large to show in full.