Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d809990832 |
@@ -20,33 +20,21 @@ jobs:
|
||||
# check.yml runs script/cibuild, which does all of its work inside
|
||||
# the digest-pinned Dockerfile images -- so without this step the
|
||||
# release either fails at the before-hook or, worse, ships binaries
|
||||
# built by whatever unpinned Go the runner happens to carry.
|
||||
# REPO_POLICIES.md requires every external reference to be pinned,
|
||||
# and script/release already refuses a goreleaser that is not the
|
||||
# pinned build; the compiler that actually produces the artifacts
|
||||
# is the last thing that should be exempt from that.
|
||||
# built by whatever Go the runner happens to carry.
|
||||
#
|
||||
# go-version-file rather than a literal: go.mod's `go 1.26.1` is
|
||||
# the single source of truth for the toolchain, the same way the
|
||||
# Dockerfile FROM line is the single source of truth for the
|
||||
# linter version that script/lint enforces. It is a three-component
|
||||
# version, so setup-go resolves it exactly -- no silent drift onto
|
||||
# a newer patch release.
|
||||
#
|
||||
# actions/setup-go v5.6.0, 2025-12-15. Pinned by commit sha, like
|
||||
# the checkout above. v5.x is a node20 action, matching the node20
|
||||
# actions/checkout v4 already in use here; the v6/v7 line requires
|
||||
# a node24 runner, which this Gitea runner has never been asked
|
||||
# for and cannot be assumed to provide.
|
||||
# actions/setup-go would pin the action by commit sha, but the Go
|
||||
# tarball it downloads at runtime is verified against no value in
|
||||
# this repo, and the action exposes no checksum input.
|
||||
# REPO_POLICIES.md requires every external reference to be pinned
|
||||
# by hash with no exceptions, and this is the compiler that
|
||||
# produces the published binaries -- the input where a substituted
|
||||
# artifact matters most. So Go is installed the way goreleaser is:
|
||||
# script/install-go downloads the exact archive for go.mod's `go`
|
||||
# directive and refuses it unless its sha256 matches the value
|
||||
# committed in the script, then puts .tool/go/bin on PATH for the
|
||||
# steps below.
|
||||
- name: Install Go
|
||||
uses: actions/setup-go@40f1582b2485089dde7abd97c1529aa768e1baff
|
||||
with:
|
||||
go-version-file: go.mod
|
||||
# setup-go's module cache needs a runner-side cache backend.
|
||||
# A release is cut rarely and a cold module download costs
|
||||
# seconds; a release failing because a cache service is absent
|
||||
# costs a re-tag. Off, deliberately.
|
||||
cache: false
|
||||
run: script/install-go
|
||||
- name: Install goreleaser
|
||||
run: script/install-goreleaser
|
||||
- name: Release
|
||||
@@ -58,3 +46,8 @@ jobs:
|
||||
# It is deliberately not the runner's automatic token, which is
|
||||
# not guaranteed to carry that scope.
|
||||
GITEA_TOKEN: ${{ secrets.RELEASE_TOKEN }}
|
||||
# Build with the toolchain install-go just verified, never a
|
||||
# different one auto-downloaded from a `toolchain` directive:
|
||||
# the point of the hash pin is that this exact compiler makes
|
||||
# the release.
|
||||
GOTOOLCHAIN: local
|
||||
|
||||
+1
-7
@@ -366,17 +366,11 @@ bucket/
|
||||
│ └── {full-hash} # Compressed+encrypted blob
|
||||
│
|
||||
└── metadata/
|
||||
└── {remote-key}/
|
||||
└── {snapshot-id}/
|
||||
├── db.zst.age # Encrypted binary SQLite database
|
||||
└── manifest.json.zst # Blob list (for pruning/verification)
|
||||
```
|
||||
|
||||
The `{remote-key}` directory name is a one-way double SHA-256 hash of the human
|
||||
snapshot ID, so the human ID (hostname, snapshot name, timestamp) is never
|
||||
written to the store as a directory name. See
|
||||
[docs/REPOSTRUCTURE.md](docs/REPOSTRUCTURE.md#remote-key-derivation) for the
|
||||
derivation and a worked example.
|
||||
|
||||
## Thread Safety
|
||||
|
||||
- `Packer`: Thread-safe via mutex. Multiple goroutines can call `AddChunk()`.
|
||||
|
||||
@@ -344,7 +344,7 @@ both are set.
|
||||
├── blobs/
|
||||
│ └── <aa>/<bb>/<full_blob_hash>
|
||||
└── metadata/
|
||||
└── <remote-key>/
|
||||
└── <snapshot_id>/
|
||||
├── db.zst.age # Encrypted binary SQLite database
|
||||
└── manifest.json.zst # Unencrypted blob list (for pruning)
|
||||
```
|
||||
@@ -355,16 +355,8 @@ both are set.
|
||||
* `manifest.json.zst` is an unencrypted compressed JSON blob list, enabling
|
||||
pruning without the private key
|
||||
|
||||
Snapshot IDs follow the human-readable format
|
||||
`<hostname>_<snapshot-name>_<RFC3339-timestamp>` (e.g.
|
||||
`server1_home_2025-06-01T12:00:00Z`), but this ID is never written to the
|
||||
destination store in plaintext. Each snapshot's metadata directory is named
|
||||
with its `<remote-key>`, a one-way double SHA-256 hash of the ID, so a listing
|
||||
of the store reveals no hostname, snapshot name, or backup time. For example,
|
||||
`server1_home_2025-06-01T12:00:00Z` is stored under
|
||||
`metadata/17f97bcde958748af076b926af59823943db59e80ce7170b40f124dfa28f64aa/`.
|
||||
See [docs/REPOSTRUCTURE.md](docs/REPOSTRUCTURE.md#remote-key-derivation) for the
|
||||
derivation.
|
||||
Snapshot IDs follow the format `<hostname>_<snapshot-name>_<RFC3339-timestamp>`
|
||||
(e.g. `server1_home_2025-06-01T12:00:00Z`).
|
||||
|
||||
### data flow
|
||||
|
||||
@@ -381,7 +373,7 @@ derivation.
|
||||
|
||||
**restore:**
|
||||
|
||||
1. Download and decrypt `metadata/<remote-key>/db.zst.age`
|
||||
1. Download and decrypt `metadata/<snapshot_id>/db.zst.age`
|
||||
2. Open the binary SQLite database
|
||||
3. Query files (optionally filtered by paths)
|
||||
4. Download and decrypt required blobs
|
||||
@@ -657,6 +649,14 @@ them. We provide:
|
||||
called by `script/bootstrap`; the release workflow calls it directly
|
||||
because it needs `goreleaser` but not the Docker daemon
|
||||
`script/bootstrap` insists on.
|
||||
* `script/install-go` — install the Go toolchain named by `go.mod`'s
|
||||
`go` directive into `.tool/go` from a sha256-verified `go.dev`
|
||||
archive, and put it on `PATH`. Idempotent. Called only by the release
|
||||
workflow, which needs a host Go for `goreleaser` to shell out to;
|
||||
nothing else on the release runner does. `actions/setup-go` is not
|
||||
used because it verifies the downloaded toolchain against no value in
|
||||
this repo. Bumping Go edits `go.mod`, the checksum in this script, and
|
||||
the `Dockerfile` `golang` digest together.
|
||||
* `script/release` — cross-compile and publish the release artifacts
|
||||
with the pinned `goreleaser`. Refuses a `goreleaser` on `PATH` whose
|
||||
version is not the pinned one, on the same reasoning as `script/lint`.
|
||||
|
||||
@@ -25,6 +25,16 @@ release" is exactly the contradiction
|
||||
|
||||
# Completed Steps
|
||||
|
||||
- 2026-09-21: Hash-verified the Go toolchain in the release workflow
|
||||
([issue #105](https://git.eeqj.de/sneak/vaultik/issues/105)). New
|
||||
`script/install-go` downloads the exact `go.dev` archive for `go.mod`'s
|
||||
`go` directive and refuses it unless its sha256 matches a value
|
||||
committed in the script; `.gitea/workflows/release.yml` calls it
|
||||
instead of `actions/setup-go`, which verified the downloaded toolchain
|
||||
against nothing in the repo. `GOTOOLCHAIN: local` on the release step
|
||||
keeps that exact compiler from auto-switching. Bumping Go now touches
|
||||
`go.mod`, the checksum, and the `Dockerfile` `golang` digest together.
|
||||
|
||||
- 2026-08-10: Moved every lint run into its own container, as a build
|
||||
step ([issue #113](https://git.eeqj.de/sneak/vaultik/issues/113)).
|
||||
New root `Dockerfile.lint`, built by `script/lint`, runs
|
||||
|
||||
+2
-4
@@ -194,10 +194,8 @@ After a snapshot is completed:
|
||||
2. Clean temporary database to contain only current snapshot data
|
||||
3. Export to SQL dump using sqlite3
|
||||
4. Compress with zstd and encrypt with age
|
||||
5. Upload to S3 as `metadata/{remote-key}/db.zst.age`
|
||||
6. Generate blob manifest and upload as `metadata/{remote-key}/manifest.json.zst`
|
||||
|
||||
The `{remote-key}` directory name is a one-way hash of the human snapshot ID, so the ID is never written to the store in plaintext; see [REPOSTRUCTURE.md](REPOSTRUCTURE.md#remote-key-derivation).
|
||||
5. Upload to S3 as `metadata/{snapshot-id}/db.zst.age`
|
||||
6. Generate blob manifest and upload as `metadata/{snapshot-id}/manifest.json.zst`
|
||||
|
||||
### 4. Restore Process
|
||||
|
||||
|
||||
+17
-37
@@ -17,13 +17,11 @@ Vaultik stores all backup data in an S3-compatible object store. The repository
|
||||
│ └── <hash[2:4]>/
|
||||
│ └── <full-hash>
|
||||
└── metadata/
|
||||
└── <remote-key>/
|
||||
└── <snapshot-id>/
|
||||
├── db.zst.age
|
||||
└── manifest.json.zst
|
||||
```
|
||||
|
||||
The metadata subdirectory is named with the **remote key**, a one-way hash of the snapshot ID, not with the human-readable snapshot ID itself. See [Remote Key Derivation](#remote-key-derivation).
|
||||
|
||||
## Blobs Directory (`blobs/`)
|
||||
|
||||
### Structure
|
||||
@@ -42,11 +40,9 @@ Blobs contain the actual file data from backups and must be encrypted for securi
|
||||
|
||||
## Metadata Directory (`metadata/`)
|
||||
|
||||
Each snapshot has its own subdirectory. The directory is **not** named with the human-readable snapshot ID; it is named with the remote key — a one-way hash of that ID. The human ID is never written to the destination store as a directory name (see [Remote Key Derivation](#remote-key-derivation)).
|
||||
Each snapshot has its own subdirectory named with the snapshot ID.
|
||||
|
||||
### Snapshot ID Format
|
||||
|
||||
The human-readable snapshot ID is used in CLI arguments, log lines, and the local database. It is not written to the destination store.
|
||||
- **Format**: `<hostname>_<snapshot-name>_<RFC3339>` (or `<hostname>_<RFC3339>` if no
|
||||
name was specified)
|
||||
- **Example**: `laptop_home_2024-01-15T14:30:52Z`
|
||||
@@ -55,19 +51,6 @@ The human-readable snapshot ID is used in CLI arguments, log lines, and the loca
|
||||
- Snapshot name from the configured `snapshots:` map (optional)
|
||||
- RFC3339 UTC timestamp
|
||||
|
||||
This ID reveals the hostname, the configured snapshot name, and the backup time, so it is never used as the on-disk directory name — the remote key is used instead.
|
||||
|
||||
### Remote Key Derivation
|
||||
|
||||
The remote key is `hex(SHA256(SHA256("vaultik|" + snapshot-id)))`: a double SHA-256 over the snapshot ID, with a `vaultik|` domain-separation prefix. The result is a 64-character hex string with no structure a remote observer can reverse. Implemented in `internal/snapshot/remotekey.go`.
|
||||
|
||||
Worked example:
|
||||
- Snapshot ID: `server1_home_2025-06-01T12:00:00Z`
|
||||
- Remote key: `17f97bcde958748af076b926af59823943db59e80ce7170b40f124dfa28f64aa`
|
||||
- Directory: `metadata/17f97bcde958748af076b926af59823943db59e80ce7170b40f124dfa28f64aa/`
|
||||
|
||||
Because the hash is one-way, a listing of the destination store reveals neither the hostname nor the snapshot name of any backup. The same remote key is stored in the manifest's `snapshot_id` field.
|
||||
|
||||
### Files in Each Snapshot Directory
|
||||
|
||||
#### `db.zst.age` - Encrypted Database
|
||||
@@ -85,17 +68,16 @@ Because the hash is one-way, a listing of the destination store reveals neither
|
||||
- **Structure**:
|
||||
```json
|
||||
{
|
||||
"snapshot_id": "17f97bcde958748af076b926af59823943db59e80ce7170b40f124dfa28f64aa",
|
||||
"timestamp": "2025-06-01T12:00:00Z",
|
||||
"snapshot_id": "laptop_home_2024-01-15T14:30:52Z",
|
||||
"timestamp": "2024-01-15T14:30:52Z",
|
||||
"blob_count": 42,
|
||||
"total_compressed_size": 1048576,
|
||||
"blobs": [
|
||||
{ "hash": "cafebabe1234567890abcdef1234567890abcdef1234567890abcdef12345678", "compressed_size": 24576 },
|
||||
{ "hash": "deadbeef1234567890abcdef1234567890abcdef1234567890abcdef12345678", "compressed_size": 32768 }
|
||||
"cafebabe1234567890abcdef1234567890abcdef1234567890abcdef12345678",
|
||||
"deadbeef1234567890abcdef1234567890abcdef1234567890abcdef12345678",
|
||||
...
|
||||
]
|
||||
}
|
||||
```
|
||||
`snapshot_id` is the remote key (a hash), not the human ID; `timestamp` is written in the clear.
|
||||
|
||||
### Why Manifest is Unencrypted
|
||||
The manifest must be readable without the private key to enable:
|
||||
@@ -104,7 +86,7 @@ The manifest must be readable without the private key to enable:
|
||||
3. **Verification** - Checking blob existence without decryption
|
||||
4. **Cross-snapshot deduplication analysis** - Finding shared blobs between snapshots
|
||||
|
||||
The manifest contains the remote key, the backup timestamp, the blob count and total compressed size, and each blob's hash and compressed size. It contains no file names, paths, or other decrypted metadata.
|
||||
The manifest only contains blob hashes, not file names or any other sensitive information.
|
||||
|
||||
## Security Considerations
|
||||
|
||||
@@ -114,21 +96,19 @@ The manifest contains the remote key, the backup timestamp, the blob count and t
|
||||
- **File-to-chunk mappings** (in db.zst.age)
|
||||
|
||||
### What's Not Encrypted
|
||||
- **The remote key** — directory names and the manifest `snapshot_id`, a one-way hash of the snapshot ID (see [Remote Key Derivation](#remote-key-derivation))
|
||||
- **The backup timestamp** (in manifest.json.zst)
|
||||
- **Blob hashes and their compressed sizes** (in manifest.json.zst)
|
||||
- **Blob count and total compressed size per snapshot** (in manifest.json.zst)
|
||||
- **Blob hashes** (in manifest.json.zst)
|
||||
- **Snapshot IDs** (directory names)
|
||||
- **Blob count per snapshot** (in manifest.json.zst)
|
||||
|
||||
### Privacy Implications
|
||||
From the unencrypted data, an observer of the destination store can determine:
|
||||
- **When each backup was taken** — not from the directory name, which is a one-way hash, but from the plaintext `timestamp` field in manifest.json.zst, which is published in the clear
|
||||
- How many blobs each snapshot references, and the total compressed size
|
||||
- The compressed size of each blob, and which blobs are shared between snapshots (deduplication patterns)
|
||||
|
||||
Together these give an observer a timing-and-size profile of every snapshot. This is an accepted, documented property of the format, not a defect: the manifest is unencrypted so that pruning can run without the private key, and the timing channel could not be closed by encrypting it anyway — object creation times and per-object sizes stay visible at the storage layer on both `s3://` and `file://` destinations regardless.
|
||||
From the unencrypted data, an observer can determine:
|
||||
- When backups were taken (from snapshot IDs)
|
||||
- Which hostname created backups (from snapshot IDs)
|
||||
- How many blobs each snapshot references
|
||||
- Which blobs are shared between snapshots (deduplication patterns)
|
||||
- The size of each encrypted blob
|
||||
|
||||
An observer cannot determine:
|
||||
- The hostname or snapshot name of any backup (the directory name and the manifest `snapshot_id` are one-way hashes of the human ID)
|
||||
- File names or paths
|
||||
- File contents
|
||||
- File permissions or ownership
|
||||
|
||||
@@ -22,9 +22,8 @@ const remoteKeyPrefix = "vaultik|"
|
||||
//
|
||||
// - the "metadata/<remote-key>/..." subdirectory on the storage
|
||||
// backend so a directory listing of the bucket / file:// dest
|
||||
// doesn't reveal hostnames or configured snapshot names. (The
|
||||
// backup time is not hidden: the manifest.json.zst inside that
|
||||
// directory carries a plaintext RFC3339 timestamp.)
|
||||
// doesn't reveal hostnames, configured snapshot names, or backup
|
||||
// timestamps;
|
||||
// - the `snapshot_id` field of the unencrypted manifest.json.zst
|
||||
// for the same reason;
|
||||
// - any code path that needs to translate a known local snapshot ID
|
||||
|
||||
@@ -840,10 +840,8 @@ func (sm *SnapshotManager) generateBlobManifest(
|
||||
}
|
||||
|
||||
// Create manifest. SnapshotID in the unencrypted manifest is the
|
||||
// double-SHA256 remote key (see RemoteSnapshotKey), not the human ID,
|
||||
// so neither this field nor the directory name reveals the hostname or
|
||||
// snapshot name. Timestamp below is written in the clear, so the backup
|
||||
// time is observable to anyone who can read the manifest.
|
||||
// double-SHA256 remote key, not the human ID, so the public bytes
|
||||
// don't reveal hostname/snapshot-name/timestamp metadata.
|
||||
manifest := &Manifest{
|
||||
SnapshotID: RemoteSnapshotKey(snapshotID),
|
||||
Timestamp: time.Now().UTC().Format(time.RFC3339),
|
||||
|
||||
Executable
+160
@@ -0,0 +1,160 @@
|
||||
#!/bin/sh
|
||||
# script/install-go: install the Go toolchain pinned by go.mod into the
|
||||
# repo-local tool directory, verified against a committed sha256. Our
|
||||
# own extension to scripts-to-rule-them-all. Idempotent: exits at once
|
||||
# when the pinned toolchain is already installed.
|
||||
#
|
||||
# Only .gitea/workflows/release.yml calls this. goreleaser is not a
|
||||
# compiler: it shells out to `go` for the `before:` hook and for every
|
||||
# one of the four cross-compiles, so the release runner needs a Go
|
||||
# toolchain on PATH. check.yml never does -- it builds inside the
|
||||
# digest-pinned Dockerfile images -- so this is the release path's only
|
||||
# host Go, and per REPO_POLICIES.md it must be pinned by hash.
|
||||
# actions/setup-go exposes no checksum input, so Go is installed the way
|
||||
# script/install-goreleaser installs goreleaser: download the exact
|
||||
# archive from go.dev and refuse it unless its sha256 matches the value
|
||||
# committed below.
|
||||
#
|
||||
# The version is go.mod's `go` directive, the single source of truth for
|
||||
# the toolchain. GO_VERSION below MUST equal it, and this script fails
|
||||
# when they disagree -- so bumping Go is one reviewed change touching
|
||||
# go.mod, the checksum here, and the Dockerfile golang digest together.
|
||||
#
|
||||
# Linux only, because that is what the release runner is. A darwin dev
|
||||
# building a snapshot uses their own Go; supporting an OS means adding
|
||||
# its checksums.
|
||||
set -eu
|
||||
|
||||
ROOT="$(cd "$(dirname "$0")/.." && pwd -P)"
|
||||
|
||||
# Go 1.26.1. Checksums are the sha256 values go.dev publishes for each
|
||||
# archive at https://go.dev/dl/ (also in its ?mode=json manifest).
|
||||
GO_VERSION="1.26.1"
|
||||
SHA256_LINUX_AMD64="031f088e5d955bab8657ede27ad4e3bc5b7c1ba281f05f245bcc304f327c987a"
|
||||
SHA256_LINUX_ARM64="a290581cfe4fe28ddd737dde3095f3dbeb7f2e4065cab4eae44dfc53b760c2f7"
|
||||
|
||||
GOROOT_DIR="$ROOT/.tool/go"
|
||||
GOCMD="$GOROOT_DIR/bin/go"
|
||||
|
||||
# The `go` directive in go.mod, e.g. "1.26.1" from `go 1.26.1`.
|
||||
gomod_go_version() {
|
||||
sed -n 's/^go \([0-9][0-9.]*\).*/\1/p' "$ROOT/go.mod" | head -n 1
|
||||
}
|
||||
|
||||
# Print the version of the go at $1 as "1.26.1", or nothing if it is not
|
||||
# usable. `go version` prints "go version go1.26.1 linux/amd64".
|
||||
go_version() {
|
||||
[ -x "$1" ] || return 0
|
||||
"$1" version 2>/dev/null |
|
||||
sed -n 's/^go version go\([0-9][0-9.]*\) .*/\1/p' |
|
||||
head -n 1
|
||||
}
|
||||
|
||||
verify_sha256() {
|
||||
file="$1"
|
||||
want="$2"
|
||||
if command -v sha256sum >/dev/null 2>&1; then
|
||||
got="$(sha256sum "$file" | cut -d' ' -f1)"
|
||||
elif command -v shasum >/dev/null 2>&1; then
|
||||
got="$(shasum -a 256 "$file" | cut -d' ' -f1)"
|
||||
else
|
||||
echo "install-go: no sha256sum or shasum available" >&2
|
||||
return 1
|
||||
fi
|
||||
if [ "$got" != "$want" ]; then
|
||||
echo "install-go: checksum mismatch for $file" >&2
|
||||
echo " expected: $want" >&2
|
||||
echo " actual: $got" >&2
|
||||
return 1
|
||||
fi
|
||||
}
|
||||
|
||||
# On a Gitea/GitHub Actions runner, put the toolchain on PATH for the
|
||||
# steps that follow by appending to the file named by $GITHUB_PATH. A
|
||||
# no-op off CI, where the caller manages its own PATH.
|
||||
export_ci_path() {
|
||||
[ -n "${GITHUB_PATH:-}" ] || return 0
|
||||
echo "$GOROOT_DIR/bin" >>"$GITHUB_PATH"
|
||||
}
|
||||
|
||||
main() {
|
||||
cd "$ROOT"
|
||||
|
||||
want="$(gomod_go_version)"
|
||||
if [ "$want" != "$GO_VERSION" ]; then
|
||||
echo "install-go: go.mod says go $want but this script pins" \
|
||||
"$GO_VERSION." >&2
|
||||
echo " Update GO_VERSION and the checksums in this script to" \
|
||||
"match go.mod." >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Already installed from a previous run? Then just fix PATH and stop.
|
||||
if [ "$(go_version "$GOCMD")" = "$GO_VERSION" ]; then
|
||||
echo "go $GO_VERSION already installed in .tool/go"
|
||||
export_ci_path
|
||||
return 0
|
||||
fi
|
||||
|
||||
os="$(uname -s)"
|
||||
arch="$(uname -m)"
|
||||
case "$os" in
|
||||
Linux) os="linux" ;;
|
||||
*)
|
||||
echo "install-go: unsupported OS $os (release runner is Linux)" >&2
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
case "$arch" in
|
||||
x86_64 | amd64)
|
||||
arch="amd64"
|
||||
sum="$SHA256_LINUX_AMD64"
|
||||
;;
|
||||
arm64 | aarch64)
|
||||
arch="arm64"
|
||||
sum="$SHA256_LINUX_ARM64"
|
||||
;;
|
||||
*)
|
||||
echo "install-go: no pinned checksum for architecture $arch" >&2
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
|
||||
archive="go${GO_VERSION}.${os}-${arch}.tar.gz"
|
||||
url="https://go.dev/dl/${archive}"
|
||||
|
||||
if ! command -v curl >/dev/null 2>&1; then
|
||||
echo "install-go: curl is required" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
dl="$(mktemp -d)"
|
||||
mkdir -p "$ROOT/.tool"
|
||||
stage="$(mktemp -d "$ROOT/.tool/.go-install.XXXXXX")"
|
||||
# shellcheck disable=SC2064 # expand the paths now, not at trap time
|
||||
trap "rm -rf '$dl' '$stage'" EXIT INT TERM
|
||||
|
||||
echo "installing go $GO_VERSION for ${os}-${arch}"
|
||||
curl -fsSL --retry 3 -o "$dl/$archive" "$url"
|
||||
verify_sha256 "$dl/$archive" "$sum"
|
||||
|
||||
# The archive unpacks to a top-level `go/` directory. Extract it into
|
||||
# a staging directory on the same filesystem as the destination, then
|
||||
# rename it into place so a concurrent run never observes a
|
||||
# half-written toolchain.
|
||||
tar -xzf "$dl/$archive" -C "$stage"
|
||||
rm -rf "$GOROOT_DIR"
|
||||
mv "$stage/go" "$GOROOT_DIR"
|
||||
|
||||
installed="$(go_version "$GOCMD")"
|
||||
if [ "$installed" != "$GO_VERSION" ]; then
|
||||
echo "install-go: installed toolchain reports '$installed'," \
|
||||
"expected '$GO_VERSION'" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "go $GO_VERSION installed to .tool/go"
|
||||
export_ci_path
|
||||
}
|
||||
|
||||
main "$@"
|
||||
Reference in New Issue
Block a user