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
11 changes: 11 additions & 0 deletions .github/dependabot.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
version: 2
updates:
- package-ecosystem: github-actions
directory: /
schedule:
interval: weekly
cooldown:
default-days: 7 # give a hijacked or retagged release time to be noticed
groups:
actions:
patterns: ["*"]
38 changes: 38 additions & 0 deletions .github/scripts/guard-remote-exec.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
#!/bin/bash

# Fails if a script or guide pipes a download straight into a shell, or fetches
# this repository's files from the mutable main branch. Operators run these
# commands as root, so every download has to be pinned and checked first.
#
# Run from anywhere: bash .github/scripts/guard-remote-exec.sh

set -euo pipefail

cd "$(dirname "$0")/../.."

paths=(scripts sysop developer community-manager README.md)

# `curl ... | sh`, `wget ... | sudo -E bash -` and similar.
pipe_to_shell='(curl|wget)[^#]*\|[[:space:]]*(sudo[[:space:]]+(-[A-Za-z]+[[:space:]]+)*)?(ba|z|da)?sh([^[:alnum:]_]|$)'
# `bash <(curl ...)`
process_substitution='(ba|z|da)?sh[[:space:]]+<\([[:space:]]*(curl|wget)'
# Raw fetches from this repo's main branch.
raw_main='raw\.githubusercontent\.com/OpenVTC/vti-setup/(refs/heads/)?main/'

status=0
check() {
local description="$1" pattern="$2"
if grep -rnE -- "$pattern" "${paths[@]}"; then
echo "::error::${description}" >&2
status=1
fi
}

check "download piped into a shell; download, verify, then run instead" "$pipe_to_shell"
check "download run through process substitution; download, verify, then run instead" "$process_substitution"
check "fetch from OpenVTC/vti-setup main; use a tagged release asset instead" "$raw_main"

if [ "$status" -eq 0 ]; then
echo "No remote-exec patterns found."
fi
exit "$status"
27 changes: 27 additions & 0 deletions .github/workflows/lint.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
name: lint

on:
push:
branches: [main]
pull_request:

permissions:
contents: read

jobs:
shell:
name: shellcheck and remote-exec guard
runs-on: ubuntu-24.04
steps:
- name: Check out
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false

- name: shellcheck
run: |
shellcheck --version
shellcheck scripts/*.sh .github/scripts/*.sh

- name: No downloads piped into a shell, no scripts fetched from main
run: bash .github/scripts/guard-remote-exec.sh
95 changes: 95 additions & 0 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
name: release

# Publishes scripts/setup-explore.sh as a GitHub Release asset with a
# SHA256SUMS file and a build provenance attestation, so operators can verify
# what they run as root. See RELEASING.md.

on:
push:
tags:
- "v*"

permissions: {}

concurrency:
group: release-${{ github.ref }}
cancel-in-progress: false

jobs:
attest:
name: Checksum and attest release assets
runs-on: ubuntu-24.04
permissions:
contents: read
id-token: write # Sigstore signing certificate for the attestation
attestations: write # store the attestation on this repository
outputs:
sha256: ${{ steps.assets.outputs.sha256 }}
steps:
- name: Check out the tagged commit
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false
fetch-depth: 0

- name: Require the tag to point at a commit on main
run: git merge-base --is-ancestor "$GITHUB_SHA" refs/remotes/origin/main

- name: Build release assets
id: assets
run: |
mkdir dist
install -m 0644 scripts/setup-explore.sh dist/setup-explore.sh
(cd dist && sha256sum setup-explore.sh > SHA256SUMS)
cat dist/SHA256SUMS
echo "sha256=$(cut -d ' ' -f 1 dist/SHA256SUMS)" >> "$GITHUB_OUTPUT"

- name: Attest build provenance
uses: actions/attest-build-provenance@4d101475d8b20a2381f78447822ac1eab6504dd8 # v4.2.2
with:
subject-path: |
dist/setup-explore.sh
dist/SHA256SUMS

publish:
name: Publish GitHub Release
needs: attest
runs-on: ubuntu-24.04
environment: release
permissions:
contents: write # create the release and upload its assets
steps:
- name: Check out the tagged commit
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false

- name: Rebuild assets and match the attested digest
env:
ATTESTED_SHA256: ${{ needs.attest.outputs.sha256 }}
run: |
mkdir dist
install -m 0644 scripts/setup-explore.sh dist/setup-explore.sh
(cd dist && sha256sum setup-explore.sh > SHA256SUMS)
printf '%s setup-explore.sh\n' "$ATTESTED_SHA256" | (cd dist && sha256sum -c -)

- name: Create the release
env:
GH_TOKEN: ${{ github.token }}
TAG: ${{ github.ref_name }}
ATTESTED_SHA256: ${{ needs.attest.outputs.sha256 }}
run: |
{
echo "setup-explore.sh SHA-256: \`${ATTESTED_SHA256}\`"
echo
echo "Check the download against the hash pinned in sysop/explore/01-server-setup.md, then verify provenance:"
echo
echo '```bash'
echo "gh attestation verify setup-explore.sh --repo ${GITHUB_REPOSITORY} --signer-workflow ${GITHUB_REPOSITORY}/.github/workflows/release.yml"
echo '```'
} > notes.md
gh release create "$TAG" dist/setup-explore.sh dist/SHA256SUMS \
--repo "$GITHUB_REPOSITORY" \
--verify-tag \
--title "$TAG" \
--notes-file notes.md
46 changes: 46 additions & 0 deletions RELEASING.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
# Releasing

`scripts/setup-explore.sh` runs as root, so the explore guide only has operators run a copy from a tagged GitHub Release, checked against a SHA-256 pinned in [`sysop/explore/01-server-setup.md`](sysop/explore/01-server-setup.md). Pushing a `v*` tag runs [`.github/workflows/release.yml`](.github/workflows/release.yml), which attaches `setup-explore.sh` and `SHA256SUMS` to the release and records a build provenance attestation for both.

## One-time repository setup

- Create the `release` environment (Settings → Environments) with required reviewers from the maintainers team. The publish job waits for that approval. Without it, GitHub creates the environment unprotected on the first run.
- Add a tag ruleset that restricts creating, updating and deleting `v*` tags to maintainers.

## Cutting a release

The pinned hash covers only the script, not the guide, so the guide can be updated before the tag exists.

1. Pick the next version, for example `v1.1.0`, and hash the script on `main`:

```bash
git switch main && git pull --ff-only
sha256sum scripts/setup-explore.sh
```

2. Open a PR that sets `VER` and `SHA256` in Step 3 of `sysop/explore/01-server-setup.md` to that version and hash. The PR must not change `scripts/setup-explore.sh`. Merge it.
3. Tag the merge commit and push the tag straight away, because the guide now points at it:

```bash
git pull --ff-only
sha256sum scripts/setup-explore.sh # must still equal the pinned hash
git tag -a v1.1.0 -m "v1.1.0" # or -s if you sign tags
git push origin v1.1.0
```

4. Approve the `release` environment deployment when the workflow asks for it.
5. Verify the published assets the way an operator will:

```bash
VER=v1.1.0
SHA256=<hash pinned in the guide>
curl -fsSLO "https://github.com/OpenVTC/vti-setup/releases/download/${VER}/setup-explore.sh"
curl -fsSLO "https://github.com/OpenVTC/vti-setup/releases/download/${VER}/SHA256SUMS"
echo "${SHA256} setup-explore.sh" | sha256sum -c -
sha256sum -c SHA256SUMS
gh attestation verify setup-explore.sh \
--repo OpenVTC/vti-setup \
--signer-workflow OpenVTC/vti-setup/.github/workflows/release.yml
```

If any check fails, delete the release and the tag, fix the pin or the script, and release again under a new version. Never move a published tag.
Loading