Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions .github/pull_request_template.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,13 @@

<!-- List checks run, results and any skips. For UI changes, add a synthetic-data screenshot. -->

## Tested on staging

<!-- Changes to the running service: deploy this branch to staging (push it to the staging
branch, or run deploy-staging with its ref), use the changed behavior there, and say
what you did and what happened. Name the staging run ID when a workflow ran. Merging
deploys to production. Docs, tests or evals only: delete this section. -->

## Notes

<!-- Mention limitations or setup changes. For integrations, link official API/auth docs,
Expand Down
51 changes: 44 additions & 7 deletions .github/workflows/deploy-staging.yml
Original file line number Diff line number Diff line change
@@ -1,40 +1,54 @@
name: deploy-staging

# Staging runs every merge to main, and any branch on request. Production deploys stay manual.
# Try a branch on staging before merging it: push it to the staging branch
# (git push -f origin my-branch:staging) or run this workflow with its ref.
# Merges to main reach staging (and then production) through the deploy workflow.
on:
push:
branches: [main]
branches: [staging]
workflow_dispatch:
inputs:
ref:
description: Branch, tag or SHA to deploy to staging
description: Branch, tag, SHA or refs/pull/<n>/head to deploy to staging
required: true
default: main

permissions:
contents: read

# One release at a time; a newer request waits instead of interrupting a deploy midway.
# One branch deploy at a time; a newer request waits instead of interrupting a deploy.
# Deploys from every workflow also wait for each other on the host itself.
concurrency:
group: deploy-staging
cancel-in-progress: false

jobs:
deploy:
runs-on: ubuntu-latest
timeout-minutes: 30
timeout-minutes: 45
# The staging environment is the only identity Google Cloud trusts for this deployer.
environment:
name: staging
url: https://app.staging.tin.computer
# No write token here: the branch's code is packaged in this job.
permissions:
contents: read
id-token: write
outputs:
sha: ${{ steps.release.outputs.sha }}
steps:
# The scripts that run as root on the host come from main, never from the branch.
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
ref: main
path: trusted
persist-credentials: false
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
ref: ${{ inputs.ref || github.sha }}
path: release
persist-credentials: false
- id: release
run: echo "sha=$(git -C release rev-parse HEAD)" >> "${GITHUB_OUTPUT}"
- uses: google-github-actions/auth@7c6bc770dae815cd3e89ee6cdf493a5fab2cc093 # v3.0.0
with:
workload_identity_provider: ${{ vars.GCP_WORKLOAD_IDENTITY_PROVIDER }}
Expand All @@ -45,4 +59,27 @@ jobs:
TIN_LITE_DEPLOY_TARGET: staging
TIN_LITE_GCP_PROJECT: ${{ vars.GCP_PROJECT }}
TIN_LITE_STATIC_IP: ${{ vars.STAGING_STATIC_IP }}
run: bash infra/deploy.sh
TIN_LITE_RELEASE_SOURCE: ${{ github.workspace }}/release
run: bash trusted/infra/deploy.sh

# The required "staging" check on main: this exact commit ran on staging. Testing it there,
# and saying what was tested in the pull request, is up to its author. This job runs no
# branch code, so the status token never reaches it.
mark:
needs: deploy
runs-on: ubuntu-latest
timeout-minutes: 5
permissions:
statuses: write
steps:
- env:
GH_TOKEN: ${{ github.token }}
REPO: ${{ github.repository }}
ACTOR: ${{ github.actor }}
SHA: ${{ needs.deploy.outputs.sha }}
run: |
[[ "${SHA}" =~ ^[0-9a-f]{40}$ ]]
gh api --method POST "repos/${REPO}/statuses/${SHA}" \
-f state=success -f context=staging \
-f description="Deployed to staging by ${ACTOR}; test it there before merging" \
-f target_url=https://app.staging.tin.computer
114 changes: 114 additions & 0 deletions .github/workflows/deploy.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
name: deploy

# A merge to main reaches production with no manual step: once ci passes on main, the same
# commit deploys to staging, and only if staging starts cleanly does it deploy to production.
# Running this workflow by hand redeploys any commit on main (for example, to roll back).
on:
workflow_run:
workflows: [ci]
types: [completed]
branches: [main]
workflow_dispatch:
inputs:
sha:
description: Full SHA of a commit on main to deploy to staging and production
required: true

permissions:
contents: read

# Newer merges include older ones: a waiting run is replaced by the newest, never interrupted.
concurrency:
group: deploy-main
cancel-in-progress: false

jobs:
release:
if: >-
github.event_name == 'workflow_dispatch' ||
(github.event.workflow_run.conclusion == 'success' &&
github.event.workflow_run.event == 'push')
runs-on: ubuntu-latest
timeout-minutes: 5
outputs:
sha: ${{ steps.pick.outputs.sha }}
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
ref: main
fetch-depth: 0
persist-credentials: false
- id: pick
env:
DISPATCHED: ${{ inputs.sha }}
FROM_CI: ${{ github.event.workflow_run.head_sha }}
run: |
sha="${DISPATCHED:-${FROM_CI}}"
if [[ ! "${sha}" =~ ^[0-9a-f]{40}$ ]]; then
echo "a full commit SHA is required" >&2
exit 1
fi
if ! git merge-base --is-ancestor "${sha}" origin/main; then
echo "${sha} is not on main" >&2
exit 1
fi
echo "sha=${sha}" >> "${GITHUB_OUTPUT}"

staging:
needs: release
runs-on: ubuntu-latest
timeout-minutes: 45
environment:
name: staging
url: https://app.staging.tin.computer
permissions:
contents: read
id-token: write
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
ref: ${{ needs.release.outputs.sha }}
persist-credentials: false
- uses: google-github-actions/auth@7c6bc770dae815cd3e89ee6cdf493a5fab2cc093 # v3.0.0
with:
workload_identity_provider: ${{ vars.GCP_WORKLOAD_IDENTITY_PROVIDER }}
service_account: ${{ vars.GCP_DEPLOY_SERVICE_ACCOUNT }}
- uses: google-github-actions/setup-gcloud@aa5489c8933f4cc7a4f7d45035b3b1440c9c10db # v3.0.1
- name: Deploy to staging
env:
TIN_LITE_DEPLOY_TARGET: staging
TIN_LITE_GCP_PROJECT: ${{ vars.GCP_PROJECT }}
TIN_LITE_STATIC_IP: ${{ vars.STAGING_STATIC_IP }}
run: bash infra/deploy.sh

production:
needs: [release, staging]
runs-on: ubuntu-latest
timeout-minutes: 45
# Only main may use this environment; Google trusts only it for the production deployer.
environment:
name: production
url: https://app.tin.computer
permissions:
contents: read
id-token: write
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
ref: ${{ needs.release.outputs.sha }}
persist-credentials: false
- uses: google-github-actions/auth@7c6bc770dae815cd3e89ee6cdf493a5fab2cc093 # v3.0.0
with:
workload_identity_provider: ${{ vars.GCP_WORKLOAD_IDENTITY_PROVIDER }}
service_account: ${{ vars.GCP_DEPLOY_SERVICE_ACCOUNT }}
- uses: google-github-actions/setup-gcloud@aa5489c8933f4cc7a4f7d45035b3b1440c9c10db # v3.0.1
- name: Deploy to production
if: vars.PROD_AUTO_DEPLOY != 'false' || github.event_name == 'workflow_dispatch'
env:
TIN_LITE_DEPLOY_TARGET: prod
TIN_LITE_GCP_PROJECT: ${{ vars.GCP_PROJECT }}
TIN_LITE_STATIC_IP: ${{ vars.PRODUCTION_STATIC_IP }}
run: bash infra/deploy.sh
- name: Report a paused production deploy
if: vars.PROD_AUTO_DEPLOY == 'false' && github.event_name != 'workflow_dispatch'
run: echo "::warning::PROD_AUTO_DEPLOY is false; production was not deployed"
47 changes: 47 additions & 0 deletions .github/workflows/staging-check.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
name: staging-check

# Sets the required "staging" status on a pull request's head commit. A change to the
# running service stays pending until that exact commit is deployed to staging
# (deploy-staging marks it). Changes that cannot affect the service pass here.
#
# pull_request_target so fork pull requests can receive a status. This job never checks out
# or runs pull request code; it only reads the list of changed paths.
on:
pull_request_target:
types: [opened, synchronize, reopened, ready_for_review]

permissions:
contents: read

jobs:
staging:
runs-on: ubuntu-latest
timeout-minutes: 5
permissions:
pull-requests: read
statuses: write
steps:
- env:
GH_TOKEN: ${{ github.token }}
REPO: ${{ github.repository }}
NUMBER: ${{ github.event.pull_request.number }}
SHA: ${{ github.event.pull_request.head.sha }}
run: |
set -euo pipefail
[[ "${NUMBER}" =~ ^[0-9]+$ && "${SHA}" =~ ^[0-9a-f]{40}$ ]]
# Paths that never reach the running service.
exempt='^(docs/|tests/|workflow_evals/|\.github/(pull_request_template\.md|review-replies\.md|CODEOWNERS)$|[^/]+\.md$|LICENSE$)'
service="$(gh api --paginate "repos/${REPO}/pulls/${NUMBER}/files" --jq '.[].filename' |
grep -vE "${exempt}" || true)"
if [[ -z "${service}" ]]; then
gh api --method POST "repos/${REPO}/statuses/${SHA}" -f state=success \
-f context=staging -f description="No change to the running service"
exit 0
fi
current="$(gh api "repos/${REPO}/commits/${SHA}/status" \
--jq '[.statuses[] | select(.context == "staging")][0].state // ""')"
if [[ "${current}" != "success" ]]; then
gh api --method POST "repos/${REPO}/statuses/${SHA}" -f state=pending \
-f context=staging \
-f description="Deploy this commit to staging and test it (deploy-staging)"
fi
5 changes: 4 additions & 1 deletion AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,10 @@ first. Workflow pull requests that don't meet it are closed automatically.
frontend framework, provider fallback or plugin marketplace as incidental work.
- Preserve unrelated edits and use an isolated worktree when another agent is working.
Do not deploy, change provider configuration, rewrite history or publish repositories
without explicit authority. A merged PR is not a production deployment.
without explicit authority. Merging to main deploys to production: once ci passes, the
deploy workflow ships that commit to staging and then production, so a merge needs the
same authority as a deploy. Run a service change on staging before merging it; see
[staging](docs/staging.md).
- Do not copy runtime dependencies from unrelated repositories. Keep the import-linter
boundaries and the explicit registry; no wildcard workflow registration.

Expand Down
5 changes: 3 additions & 2 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -36,8 +36,9 @@ Do not use it to skip review of external contributions. See the

PR tests run on GitHub-hosted runners with a read-only repository token and no production
credentials. A maintainer may need to approve a first-time contributor's CI run; that only
allows the tests to run, not the PR to merge or deploy. Live provider tests and deployment
remain separate maintainer operations.
allows the tests to run, not the PR to merge or deploy. A merge to main deploys to
production, so maintainers run a service change on [staging](docs/staging.md) before
merging it. Live provider tests remain a separate maintainer operation.

## Contributing an integration

Expand Down
30 changes: 26 additions & 4 deletions docs/repository-policy.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# Repository contribution policy

The simple path is: focused PR, passing `verify` checks, one Tin review, merge.
The simple path is: focused PR, passing `verify` checks, tested on staging, one Tin review, merge.
Bug fixes, docs and integration contributions do not need an approved issue first.
Discuss architecture changes before starting a large implementation.

Expand All @@ -24,8 +24,24 @@ Any future authorized release-history rewrite needs a deliberate temporary rules
and a verified private backup. Repository admins can edit settings; branch protection
is not a defense against an administrator intentionally changing the policy.

These settings are GitHub repository rules, not deployment automation. A merge does not
deploy the application. Existing operator-controlled deployments are unchanged.
## Merging deploys

A merge to `main` deploys the application. When `ci` passes on `main`, the `deploy`
workflow ships that commit to staging and, if staging starts cleanly, the same commit to
production. There is no separate release step, so test before merging:

- The required `staging` status passes on its own for changes that cannot reach the
running service (docs, tests, evals, Markdown). Anything else stays pending until that
exact commit is deployed to staging by `deploy-staging`. Every new push resets it.
- Deploying marks the commit; it does not prove the change works. The PR's
**Tested on staging** section says what was exercised there and what happened, and the
reviewer judges it.
- The maintainer bypass covers this check too. Skipping staging for a service change is
the same as deploying it untested.
- Set the repository variable `PROD_AUTO_DEPLOY=false` to stop automatic production
deploys; merges then stop at staging. Running `deploy` by hand with a commit on `main`
redeploys it, which is also how to roll back. Nothing rolls back on its own: a release
may have changed the catalog for good, so a person chooses forward or back.

## CI for contributions

Expand All @@ -37,6 +53,9 @@ deployment. Superseded PR checks are cancelled; main-branch checks run independe
Do not execute contributor code with `pull_request_target`, production environments,
privileged self-hosted runners or a secret-bearing follow-up workflow. Live-provider
acceptance stays a separate maintainer-run check against authorized test resources.
`staging-check` uses `pull_request_target` only to read a PR's changed paths and set its
status; it never checks out the code. Deploying an outside PR to staging runs it next to
real provider credentials, so a maintainer reads the diff first.

When public, use **Require approval for first-time contributors** for fork workflow
runs. Approving CI means allowing code to run in this test environment, not approving
Expand All @@ -46,7 +65,10 @@ See [GitHub's fork approval guidance](https://docs.github.com/en/actions/how-tos

## Repository settings

- Require the verify check, one code-owner review and resolved review threads on main.
- Require the verify and staging checks, one code-owner review and resolved review threads
on main.
- Restrict the `production` environment to `main`; Google Cloud trusts only that
environment for the production deployer, and only `staging` for the staging deployer.
- Limit the review/check bypass to the named Tin maintainer team.
- Independently block force-pushes and deletion of main with no bypass actors.
- Keep the default Actions token read-only and prevent Actions from approving PRs.
Expand Down
Loading
Loading