Merge branch 'worktree-agent-a4d35ee70c50683d0' into claude/cool-newton-7nm8r9
# Conflicts: # apps/docs/src/content/docs/guides/actions.md
| 1 | + | # Publishes g1t's own actions: each folder of actions/ (deploy, detect…) | |
| 2 | + | # is pushed, as the whole of its own history, to the repository of the | |
| 3 | + | # same name in the g1t workspace on g1t.sh (actions/deploy to g1t/deploy), | |
| 4 | + | # and its major tag, v1, is moved to it. Workflows anywhere use them as | |
| 5 | + | # `uses: g1t/deploy@v1`. A change to an action that breaks how it is | |
| 6 | + | # used gets a new major tag here (MAJOR below), and the old one stays. | |
| 7 | + | # | |
| 8 | + | # Runs on a push to main that changes actions/, and by hand. | |
| 9 | + | # | |
| 10 | + | # Needs the repository secret G1T_ACTIONS_PUBLISH_TOKEN: an access token of | |
| 11 | + | # the g1t workspace with contents: write on its action repositories | |
| 12 | + | # (g1t/deploy, g1t/detect, and one for each new folder, made public | |
| 13 | + | # before its first release). The job's own token reaches this repository | |
| 14 | + | # only, so it cannot push there. | |
| 15 | + | name: Actions release | |
| 16 | + | ||
| 17 | + | on: | |
| 18 | + | push: | |
| 19 | + | branches: [main] | |
| 20 | + | paths: | |
| 21 | + | - actions/** | |
| 22 | + | workflow_dispatch: | |
| 23 | + | ||
| 24 | + | permissions: | |
| 25 | + | contents: read | |
| 26 | + | ||
| 27 | + | concurrency: | |
| 28 | + | group: actions-release | |
| 29 | + | cancel-in-progress: false | |
| 30 | + | ||
| 31 | + | env: | |
| 32 | + | MAJOR: v1 | |
| 33 | + | NAMESPACE: g1t | |
| 34 | + | ||
| 35 | + | jobs: | |
| 36 | + | publish: | |
| 37 | + | name: Publish g1t/<action> | |
| 38 | + | runs-on: ubuntu-latest | |
| 39 | + | timeout-minutes: 15 | |
| 40 | + | steps: | |
| 41 | + | - uses: actions/checkout@v5 | |
| 42 | + | with: | |
| 43 | + | fetch-depth: 0 | |
| 44 | + | - name: Push each action and move its major tag | |
| 45 | + | env: | |
| 46 | + | PUBLISH_TOKEN: ${{ secrets.G1T_ACTIONS_PUBLISH_TOKEN }} | |
| 47 | + | SERVER: ${{ github.server_url }} | |
| 48 | + | run: | | |
| 49 | + | set -euo pipefail | |
| 50 | + | if [ -z "${PUBLISH_TOKEN}" ]; then | |
| 51 | + | echo "::error::Set the repository secret G1T_ACTIONS_PUBLISH_TOKEN (this workflow's header says what it needs)." | |
| 52 | + | exit 1 | |
| 53 | + | fi | |
| 54 | + | echo "::add-mask::${PUBLISH_TOKEN}" | |
| 55 | + | auth="Authorization: Basic $(printf 'g1t:%s' "${PUBLISH_TOKEN}" | base64 -w0)" | |
| 56 | + | git config user.name g1t | |
| 57 | + | git config user.email g1t@users.noreply.g1t.sh | |
| 58 | + | for dir in actions/*/; do | |
| 59 | + | name="$(basename "${dir}")" | |
| 60 | + | [ -f "${dir}action.yml" ] || { echo "::warning::${dir} has no action.yml; skipped."; continue; } | |
| 61 | + | commit="$(git subtree split --prefix "actions/${name}" HEAD)" | |
| 62 | + | remote="${SERVER}/${NAMESPACE}/${name}.git" | |
| 63 | + | echo "Publishing actions/${name} (${commit}) to ${NAMESPACE}/${name}" | |
| 64 | + | git -c http.extraHeader="${auth}" push "${remote}" "${commit}:refs/heads/main" | |
| 65 | + | git -c http.extraHeader="${auth}" push --force "${remote}" "${commit}:refs/tags/${MAJOR}" | |
| 66 | + | echo "- \`${NAMESPACE}/${name}@${MAJOR}\` is ${commit}" >> "${GITHUB_STEP_SUMMARY}" | |
| 67 | + | done |
| 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] | |
| 460 | 460 | Op::Deployments(DeploymentsOp::CreateDeploymentStatus), | |
| 461 | 461 | Op::Deployments(DeploymentsOp::ListEnvironments), | |
| 462 | 462 | Op::Deployments(DeploymentsOp::GetEnvironment), | |
| 463 | + | Op::Deployments(DeploymentsOp::StartDeploymentBuild), | |
| 463 | 464 | ], | |
| 464 | 465 | ), | |
| 465 | 466 | ( |
| 707 | 707 | } | |
| 708 | 708 | ||
| 709 | 709 | impl Op { | |
| 710 | − | pub const ALL: [Op; 350] = [ | |
| 710 | + | pub const ALL: [Op; 351] = [ | |
| 711 | 711 | Op::Whoami, | |
| 712 | 712 | Op::GetWorkspace, | |
| 713 | 713 | Op::CreateWorkspace, | |
| ⋯ | |||
| 983 | 983 | Op::Deployments(DeploymentsOp::CreateDeploymentStatus), | |
| 984 | 984 | Op::Deployments(DeploymentsOp::ListEnvironments), | |
| 985 | 985 | Op::Deployments(DeploymentsOp::GetEnvironment), | |
| 986 | + | Op::Deployments(DeploymentsOp::StartDeploymentBuild), | |
| 986 | 987 | Op::Artifacts(ArtifactsOp::ListArtifacts), | |
| 987 | 988 | Op::Artifacts(ArtifactsOp::ListRunArtifacts), | |
| 988 | 989 | Op::Artifacts(ArtifactsOp::GetArtifact), | |
| 10567 | 10567 | }, | |
| 10568 | 10568 | "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." | |
| 10569 | 10569 | }, | |
| 10570 | + | "start_deployment_build": { | |
| 10571 | + | "params": { | |
| 10572 | + | "owner": "flagon-io", | |
| 10573 | + | "name": "hello" | |
| 10574 | + | }, | |
| 10575 | + | "request": { | |
| 10576 | + | "project": "hello" | |
| 10577 | + | }, | |
| 10578 | + | "response": { | |
| 10579 | + | "deploy_id": "dpl_01kq9c4e6g8j0m2p4r6t8v0x2z", | |
| 10580 | + | "token": "…", | |
| 10581 | + | "kind": "preview", | |
| 10582 | + | "project": "hello", | |
| 10583 | + | "branch": "new-header", | |
| 10584 | + | "number": 42, | |
| 10585 | + | "commit": "9f2c4e6a8b0d1f3e5a7c9b1d3f5e7a9c0b2d4f6e", | |
| 10586 | + | "url": "https://hello-git-new-header-flagonio.g1t.page", | |
| 10587 | + | "trusted": true, | |
| 10588 | + | "root_dir": "", | |
| 10589 | + | "build_command": null, | |
| 10590 | + | "output_dir": null, | |
| 10591 | + | "build_env": { | |
| 10592 | + | "PUBLIC_API_URL": "https://api.example.com" | |
| 10593 | + | }, | |
| 10594 | + | "build_secrets": { | |
| 10595 | + | "SENTRY_AUTH_TOKEN": "…" | |
| 10596 | + | } | |
| 10597 | + | }, | |
| 10598 | + | "notes": "Called from a workflow step with the job's own token, `${{ github.token }}`, in a job with `permissions: deployments: write`; the [`g1t/deploy`](/guides/deployments/#deploying-from-a-workflow) action calls it, masks the token and the secrets, and runs `g1t-runner deploy` with them. Any other token is refused. This is a pull request's run, so it builds the pull request's preview; the same workflow on the production branch builds production. See [Deploying from a workflow](/guides/deployments/#deploying-from-a-workflow)." | |
| 10599 | + | }, | |
| 10570 | 10600 | "get_environment": { | |
| 10571 | 10601 | "params": { | |
| 10572 | 10602 | "owner": "flagon-io", |
| 359 | 359 | &[("environment", "environment"), ("ref", "ref"), ("sha", "sha"), ("task", "task"), ("state", "state"), ("source", "source"), ("creator", "creator"), ("page", "page"), ("per_page", "per_page")], | |
| 360 | 360 | ), | |
| 361 | 361 | route("POST", "/repos/:owner/:name/deployments", Op::Deployments(DeploymentsOp::CreateDeployment), &[]), | |
| 362 | + | // A g1t Actions job opening a g1t.page build it runs itself. | |
| 363 | + | route("POST", "/repos/:owner/:name/deployments/builds", Op::Deployments(DeploymentsOp::StartDeploymentBuild), &[]), | |
| 362 | 364 | route("GET", "/repos/:owner/:name/deployments/:id", Op::Deployments(DeploymentsOp::GetDeployment), &[]), | |
| 363 | 365 | route("GET", "/repos/:owner/:name/deployments/:id/statuses", Op::Deployments(DeploymentsOp::ListDeploymentStatuses), &[]), | |
| 364 | 366 | route("POST", "/repos/:owner/:name/deployments/:id/statuses", Op::Deployments(DeploymentsOp::CreateDeploymentStatus), &[]), |
| 263 | 263 | a("create_deployment_status", Op::Deployments(DeploymentsOp::CreateDeploymentStatus), "Report where a deployment is: in_progress, success, failure"), | |
| 264 | 264 | a("list_environments", Op::Deployments(DeploymentsOp::ListEnvironments), "Environments with their current and latest deployments"), | |
| 265 | 265 | a("get_environment", Op::Deployments(DeploymentsOp::GetEnvironment), "One environment by name, with its protection rules"), | |
| 266 | + | a("start_deployment_build", Op::Deployments(DeploymentsOp::StartDeploymentBuild), "From a workflow job: open a g1t.page build it runs itself"), | |
| 266 | 267 | a("update_environment", Op::Protection(ProtectionOp::UpdateEnvironment), "Set an environment's reviewers, wait timer and branches"), | |
| 267 | 268 | a("delete_environment", Op::Protection(ProtectionOp::DeleteEnvironment), "Remove an environment's protection rules"), | |
| 268 | 269 | a("pending_deployments", Op::Protection(ProtectionOp::GetPendingDeployments), "The environments holding a run's jobs"), |
| 242 | 242 | ||
| 243 | 243 | Actions that a composite action uses are downloaded when it runs. | |
| 244 | 244 | ||
| 245 | + | ### g1t's own actions | |
| 246 | + | ||
| 247 | + | The `g1t` workspace on g1t.sh holds g1t's own actions, public, with a | |
| 248 | + | `v1` tag that moves with each compatible release: | |
| 249 | + | ||
| 250 | + | | Action | What it does | | |
| 251 | + | | --- | --- | | |
| 252 | + | | [`g1t/deploy@v1`](https://g1t.sh/g1t/deploy) | Builds the checkout and puts it on [g1t.page](/guides/deployments/): production from the project's production branch, a preview from a pull request or another branch. The job needs `deployments: write`. See [deploying from a workflow](/guides/deployments/#deploying-from-a-workflow). | | |
| 253 | + | | [`g1t/detect@v1`](https://g1t.sh/g1t/detect) | Says what that build would find (`kind`, `install`, `build`, `output-dir`) without building anything. | | |
| 254 | + | ||
| 255 | + | Both are composite actions around `g1t-runner`, which g1t's machines | |
| 256 | + | have; on a [self-hosted runner](/guides/self-hosted-runners/) they use | |
| 257 | + | the one on `PATH` or download the newest release. | |
| 258 | + | ||
| 245 | 259 | ### Secrets for a called workflow | |
| 246 | 260 | ||
| 247 | 261 | A called workflow gets only the secrets its caller passes, plus |
| 191 | 191 | The [checklist on the overview](/guides/projects/#get-to-production) also | |
| 192 | 192 | tracks the first production deploy, a custom domain and a first preview. | |
| 193 | 193 | ||
| 194 | + | ## Deploying from a workflow | |
| 195 | + | ||
| 196 | + | A [workflow](/guides/actions/) job can build and deploy the project | |
| 197 | + | itself, with g1t's own action, `g1t/deploy`. Use it when the build needs | |
| 198 | + | steps of yours first (generated code, a test that must pass, a tool the | |
| 199 | + | default build lacks), or to deploy from a branch without a pull request. | |
| 200 | + | The app, its address, its checks and its custom domains are the same as | |
| 201 | + | for the builds g1t starts by itself. | |
| 202 | + | ||
| 203 | + | ```yaml | |
| 204 | + | # .g1t/workflows/deploy.yml | |
| 205 | + | name: Deploy | |
| 206 | + | ||
| 207 | + | on: | |
| 208 | + | push: | |
| 209 | + | branches: [main] | |
| 210 | + | pull_request: | |
| 211 | + | ||
| 212 | + | permissions: | |
| 213 | + | contents: read | |
| 214 | + | deployments: write | |
| 215 | + | ||
| 216 | + | jobs: | |
| 217 | + | deploy: | |
| 218 | + | runs-on: ubuntu-latest | |
| 219 | + | steps: | |
| 220 | + | - uses: actions/checkout@v5 | |
| 221 | + | - uses: actions/setup-node@v4 | |
| 222 | + | with: | |
| 223 | + | node-version: 24 | |
| 224 | + | - run: npm ci | |
| 225 | + | - run: npm test | |
| 226 | + | - id: deploy | |
| 227 | + | uses: g1t/deploy@v1 | |
| 228 | + | with: | |
| 229 | + | build-command: npm run build | |
| 230 | + | - run: echo "Live at ${{ steps.deploy.outputs.url }}" | |
| 231 | + | ``` | |
| 232 | + | ||
| 233 | + | 1. Turn Deployments on for the project, as in | |
| 234 | + | [Turn on deployments](#turn-on-deployments). | |
| 235 | + | 2. Add a workflow like the one above. The job needs `deployments: write` | |
| 236 | + | (or `pages: write`) in its `permissions:`. | |
| 237 | + | 3. Push. The run on `main` deploys production; a pull request's run | |
| 238 | + | deploys its preview. | |
| 239 | + | ||
| 240 | + | What the run builds: | |
| 241 | + | ||
| 242 | + | | The run | Builds | | |
| 243 | + | | --- | --- | | |
| 244 | + | | On the project's production branch (a push, `workflow_dispatch` or `schedule` there) | Production | | |
| 245 | + | | For a pull request (`pull_request`, `pull_request_target`) | That pull request's preview, at its branch's address, or `pr-<number>` for one from a fork | | |
| 246 | + | | On another branch | That branch's preview | | |
| 247 | + | | For a tag | Nothing: the step fails and says to run the workflow on a branch | | |
| 248 | + | ||
| 249 | + | `g1t/deploy` takes these inputs: | |
| 250 | + | ||
| 251 | + | | Input | Default | | | |
| 252 | + | | --- | --- | --- | | |
| 253 | + | | `project` | The repository's one project with Deployments on | The project's slug. Needed when the repository has several. | | |
| 254 | + | | `root-dir` | The project's **Root directory** | Where the project is in the checkout. | | |
| 255 | + | | `build-command` | The project's **Build command**, else what the build finds | What builds it, run after the install. | | |
| 256 | + | | `output-dir` | The project's **Output directory**, else the first folder found | The folder a static build writes. | | |
| 257 | + | | `sha` | The run's commit | The commit the `g1t / deploy` check goes on. | | |
| 258 | + | | `token` | `${{ github.token }}` | The job's own token. No other token can open a build. | | |
| 259 | + | ||
| 260 | + | Its outputs are `url` (the app's address), `deployment-id` (`dpl_…`) and | |
| 261 | + | `kind` (`production` or `preview`). | |
| 262 | + | ||
| 263 | + | The action calls | |
| 264 | + | [`POST /repos/{owner}/{repo}/deployments/builds`](/reference/api/deployments/start-deployment-build/), | |
| 265 | + | which checks the plan and the spend limit as any build does, sets the | |
| 266 | + | commit's `g1t / deploy` check to pending, and answers with a token for | |
| 267 | + | that build alone and the project's | |
| 268 | + | [variables and secrets](#secrets-and-variables) for production or | |
| 269 | + | previews. The action masks the token and every secret in the log, then | |
| 270 | + | runs `g1t-runner deploy` in the checkout, which installs, builds and | |
| 271 | + | uploads exactly as [What deploys](#what-deploys) says; its output is in | |
| 272 | + | the step's log and on the deployment's page. When the build fails, or | |
| 273 | + | g1t does not take what it built, the step fails and the deployment says | |
| 274 | + | why. | |
| 275 | + | ||
| 276 | + | - **Secrets.** A pull request from a fork or from someone without the | |
| 277 | + | Write role, and a run that had to be | |
| 278 | + | [approved](/guides/actions/#pull-requests), build without | |
| 279 | + | the project's secrets, as previews of them always have. Their token is | |
| 280 | + | read-only, so they cannot open a build at all unless the workflow runs | |
| 281 | + | on `pull_request_target`. | |
| 282 | + | - **Cost.** The build runs in the job, so its time is the job's | |
| 283 | + | [Actions time](/guides/usage-and-billing/), free on a | |
| 284 | + | [self-hosted runner](/guides/self-hosted-runners/), and is not charged | |
| 285 | + | again as build seconds. Requests, CPU time and custom domains are | |
| 286 | + | charged as for any app. | |
| 287 | + | - **g1t's own builds still run.** A push to the production branch and a | |
| 288 | + | pull request's push still start g1t's build too. Whichever starts | |
| 289 | + | later replaces the other, so the app ends up at the newest commit | |
| 290 | + | either way. | |
| 291 | + | - **On your own machines**, the action uses the `g1t-runner` on `PATH`, or | |
| 292 | + | downloads the newest release and checks it against its `SHA256SUMS`. It | |
| 293 | + | runs on Linux and macOS, with `curl` and `jq` or `python3`. | |
| 294 | + | ||
| 295 | + | `g1t/detect` says what a build of the checkout would find, without | |
| 296 | + | building anything: its outputs are `kind` (`workers`, `static`, `html` or | |
| 297 | + | `none`), `install`, `build` and `output-dir`. Use them to cache, or to | |
| 298 | + | fail early: | |
| 299 | + | ||
| 300 | + | ```yaml | |
| 301 | + | - id: detect | |
| 302 | + | uses: g1t/detect@v1 | |
| 303 | + | - if: steps.detect.outputs.kind == 'none' | |
| 304 | + | run: | | |
| 305 | + | echo "::error::Nothing here would be served: add an index.html or a build script." | |
| 306 | + | exit 1 | |
| 307 | + | ``` | |
| 308 | + | ||
| 194 | 309 | ## Custom domains | |
| 195 | 310 | ||
| 196 | 311 | Production can be served at a domain of your own, such as `example.com` or | |
| ⋯ | |||
| 352 | 467 | | | Costs g1t | You pay | | |
| 353 | 468 | | --- | --- | --- | | |
| 354 | 469 | | Projects, previews and the apps behind them | Next to nothing | Not charged | | |
| 355 | − | | Builds, by the second, whether they succeed or fail | About $0.001 a minute | About $0.0012 a minute | | |
| 470 | + | | Builds g1t runs, by the second, whether they succeed or fail (a [workflow's build](#deploying-from-a-workflow) is its job's Actions time instead) | About $0.001 a minute | About $0.0012 a minute | | |
| 356 | 471 | | Requests, to all of the workspace's apps | $0.30 a million | $0.36 a million | | |
| 357 | 472 | | CPU time your code spends computing (waiting on the network is not counted) | $0.02 a million ms | $0.024 a million ms | | |
| 358 | 473 | | Custom domains, with their certificates (a www/apex pair is two) | $0.10 a month | $0.12 a month, by the most the workspace had at once that month | | |
| ⋯ | |||
| 450 | 565 | | A domain stays at **Waiting for DNS** | Check the record against the one listed, remove other `A`/`AAAA` records for the same name, then choose **Check now**. | | |
| 451 | 566 | | A domain says "This domain is not set up" | It points at g1t, but no project has added it. Add it under **Settings → Domains**. | | |
| 452 | 567 | | "This pull request's commit no longer exists" | The pull request's head was force-pushed over, or its branch deleted, after the build was asked for. Push to the pull request again, or close it. g1t does not try that commit again. | | |
| 568 | + | | g1t/deploy says "The job's token needs `deployments: write`" | Add `deployments: write` to the workflow's or the job's `permissions:`. A pull request's run from outside gets a read-only token and cannot deploy. | | |
| 569 | + | | g1t/deploy says "Name the project to build" | The repository has several projects with Deployments on: give `project:`. | | |
| 570 | + | | g1t/deploy says "g1t.page deploys branches and pull requests" | The run is for a tag. Run the workflow on a branch, such as on a push to `main`. | | |
| 453 | 571 | | "The build did not finish in 45 minutes" | The build stopped reporting: a build stops at 30 minutes, and one not heard from after 45 is failed. Make the build faster, or build less for previews with **Build command**. | | |
| 454 | 572 | ||
| 455 | 573 | ## Running your own g1t | |
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.