diff --git a/.github/workflows/ci-integration.yml b/.github/workflows/ci-integration.yml index ceb15e2..f530a7b 100644 --- a/.github/workflows/ci-integration.yml +++ b/.github/workflows/ci-integration.yml @@ -51,9 +51,14 @@ on: permissions: contents: read -# DAAF_BRANCH tells all lifecycle scripts which branch to download from and -# target. Resolves to the branch name for schedule/dispatch, or the tag name -# for release-tag pushes (git treats tags as valid refs for --branch). +# DAAF_BRANCH tells install/migrate/rebuild scripts which ref to clone from. +# Resolves to the branch name for schedule/dispatch, or the tag name for +# release-tag pushes (git clone -b accepts both). +# +# update_daaf.sh requires a BRANCH (not a tag) because it merges upstream +# changes. Steps that invoke update_daaf.sh override DAAF_BRANCH to "" so +# the script auto-detects main/master. See "env: DAAF_BRANCH" overrides below. +# # DAAF_NESTED suppresses the interactive pause-before-exit in all scripts. env: DAAF_BRANCH: ${{ github.ref_name }} @@ -153,6 +158,8 @@ jobs: # --- Update (no-op on fresh install) --- - name: Run update_daaf.sh (no-op on fresh install) working-directory: daaf-docker + env: + DAAF_BRANCH: "" run: bash update_daaf.sh - name: Verify container running after update @@ -238,6 +245,8 @@ jobs: # --- Update with no upstream changes --- - name: Run update (no-op after fresh install) working-directory: daaf-docker + env: + DAAF_BRANCH: "" run: bash update_daaf.sh 2>&1 | tee /tmp/update_noop.txt - name: Verify already up to date @@ -746,6 +755,8 @@ jobs: # (merge) when it detects local commits ahead of upstream. - name: Update with local-only commits working-directory: daaf-docker + env: + DAAF_BRANCH: "" run: | bash update_daaf.sh > /tmp/update_local_commits.txt 2>&1 cat /tmp/update_local_commits.txt @@ -766,6 +777,8 @@ jobs: # a second one. The second should fail with the lock error. - name: Test concurrent update lock working-directory: daaf-docker + env: + DAAF_BRANCH: "" run: | bash update_daaf.sh & BG_PID=$! diff --git a/scripts/host/update_daaf.ps1 b/scripts/host/update_daaf.ps1 index 931a2ed..b077df6 100644 --- a/scripts/host/update_daaf.ps1 +++ b/scripts/host/update_daaf.ps1 @@ -829,6 +829,20 @@ if ($OriginUrl -notlike "*$UpstreamRepo*") { Write-Host "" } +# ===================================================================== +# Ensure fetch refspec covers all branches +# ===================================================================== +# git clone --depth 1 -b implies --single-branch, which locks the +# fetch refspec to only the cloned ref. This means git fetch will never +# retrieve other branches (like main), breaking auto-detect. Widen to +# the standard wildcard if it's currently narrow. +$CurrentRefspec = Invoke-ComposeGit config --get "remote.$UpstreamRemote.fetch" +if ($LASTEXITCODE -eq 0 -and $CurrentRefspec -and + $CurrentRefspec.Trim() -ne "+refs/heads/*:refs/remotes/$UpstreamRemote/*") { + Invoke-ComposeGitNull config --replace-all "remote.$UpstreamRemote.fetch" ` + "+refs/heads/*:refs/remotes/$UpstreamRemote/*" +} + # ===================================================================== # Fetch latest # ===================================================================== @@ -878,6 +892,27 @@ if ($RemoteBranch) { # User specified a branch - verify it exists on the remote $null = Invoke-ComposeGit rev-parse --verify "$UpstreamRemote/$RemoteBranch" if ($LASTEXITCODE -ne 0) { + + # Check if the value is a version tag rather than a branch. + # Tags live in refs/tags/, not refs/remotes/origin/, so the branch + # check above correctly fails for them. + $null = Invoke-ComposeGit rev-parse --verify "refs/tags/$RemoteBranch" + if ($LASTEXITCODE -eq 0) { + Write-Host "" + Write-Host "'$RemoteBranch' is a version tag, not a branch." -ForegroundColor Yellow + Write-Host "" + Write-Host "The updater needs a branch to pull changes from. Tags are fixed" + Write-Host "snapshots and cannot receive updates." + Write-Host "" + Write-Host "To update to the latest release on the main branch:" + Write-Host " .\update_daaf.ps1" + Write-Host " (without setting `$env:DAAF_BRANCH)" + Write-Host "" + Write-Host "To update from a specific branch:" + Write-Host " `$env:DAAF_BRANCH = 'dev'; .\update_daaf.ps1" + Wait-AndExit 1 + } + Write-Host "" Write-Host "The branch '$RemoteBranch' (from DAAF_BRANCH) was not found on" -ForegroundColor Red Write-Host "$UpstreamRemote." diff --git a/scripts/host/update_daaf.sh b/scripts/host/update_daaf.sh index f3119a3..b079399 100755 --- a/scripts/host/update_daaf.sh +++ b/scripts/host/update_daaf.sh @@ -717,6 +717,25 @@ if ! echo "${ORIGIN_URL}" | grep -qi "${UPSTREAM_REPO}"; then echo "" fi +# ===================================================================== +# Ensure fetch refspec covers all branches +# ===================================================================== +# git clone --depth 1 -b implies --single-branch, which locks the +# fetch refspec to only the cloned ref. This means git fetch will never +# retrieve other branches (like main), breaking auto-detect. Widen to +# the standard wildcard if it's currently narrow. +CURRENT_REFSPEC=$(docker compose exec -T daaf-docker \ + git -C /daaf config --get remote."${UPSTREAM_REMOTE}".fetch \ + /dev/null | tr -d '\r' || true) + +if [ -n "${CURRENT_REFSPEC}" ] \ + && [ "${CURRENT_REFSPEC}" != "+refs/heads/*:refs/remotes/${UPSTREAM_REMOTE}/*" ]; then + docker compose exec -T daaf-docker \ + git -C /daaf config --replace-all remote."${UPSTREAM_REMOTE}".fetch \ + "+refs/heads/*:refs/remotes/${UPSTREAM_REMOTE}/*" /dev/null \ + || true +fi + # ===================================================================== # Fetch latest # ===================================================================== @@ -757,6 +776,28 @@ if [ -n "${REMOTE_BRANCH}" ]; then if ! docker compose exec -T daaf-docker \ git -C /daaf rev-parse --verify "${UPSTREAM_REMOTE}/${REMOTE_BRANCH}" \ /dev/null 2>&1; then + + # Check if the value is a version tag rather than a branch. + # Tags live in refs/tags/, not refs/remotes/origin/, so the branch + # check above correctly fails for them. + if docker compose exec -T daaf-docker \ + git -C /daaf rev-parse --verify "refs/tags/${REMOTE_BRANCH}" \ + /dev/null 2>&1; then + echo "" + echo "'${REMOTE_BRANCH}' is a version tag, not a branch." + echo "" + echo "The updater needs a branch to pull changes from. Tags are fixed" + echo "snapshots and cannot receive updates." + echo "" + echo "To update to the latest release on the main branch:" + echo " bash update_daaf.sh" + echo " (without setting DAAF_BRANCH)" + echo "" + echo "To update from a specific branch:" + echo " DAAF_BRANCH=dev bash update_daaf.sh" + exit 1 + fi + echo "" echo "The branch '${REMOTE_BRANCH}' (from DAAF_BRANCH) was not found on" echo "${UPSTREAM_REMOTE}." diff --git a/tests/bash/update_daaf.bats b/tests/bash/update_daaf.bats index 8e6521a..5df2e97 100755 --- a/tests/bash/update_daaf.bats +++ b/tests/bash/update_daaf.bats @@ -833,6 +833,7 @@ setup_state_machine() { *"compose exec"*"fetch"*) return 0 ;; *"compose exec"*"rev-parse --verify"*"backup/"*) return 1 ;; *"compose exec"*"rev-parse --verify"*"origin/nonexistent-branch-xyz"*) return 1 ;; + *"compose exec"*"rev-parse --verify"*"refs/tags/nonexistent-branch-xyz"*) return 1 ;; *"compose exec"*"branch"*) return 0 ;; *"compose exec"*"rev-parse"*"HEAD"*) echo "abc123" ;; *) return 0 ;; @@ -844,4 +845,66 @@ setup_state_machine() { assert_failure assert_output --partial "nonexistent-branch-xyz" assert_output --partial "was not found" + refute_output --partial "version tag" +} + +@test "update: DAAF_BRANCH is a tag gives tag-specific error" { + setup_state_machine + run bash -c ' + export DAAF_BRANCH="v2.1.0" + docker() { + local all_args="$*" + case "$all_args" in + "info") return 0 ;; + *"compose ps"*"--format"*) echo "daaf-docker" ;; + *"compose exec"*"true"*) return 0 ;; + *"compose exec"*"test -f"*"/daaf/CLAUDE.md"*) return 0 ;; + *"compose exec"*"test -f"*"/daaf/.git/shallow"*) return 1 ;; + *"compose exec"*"remote get-url"*"origin"*) echo "https://github.com/DAAF-Contribution-Community/daaf.git" ;; + *"compose exec"*"fetch"*) return 0 ;; + *"compose exec"*"rev-parse --verify"*"backup/"*) return 1 ;; + *"compose exec"*"rev-parse --verify"*"origin/v2.1.0"*) return 1 ;; + *"compose exec"*"rev-parse --verify"*"refs/tags/v2.1.0"*) return 0 ;; + *"compose exec"*"branch"*) return 0 ;; + *"compose exec"*"rev-parse"*"HEAD"*) echo "abc123" ;; + *) return 0 ;; + esac + } + export -f docker + bash "'"${REPO_ROOT}"'/scripts/host/update_daaf.sh" + ' + assert_failure + assert_output --partial "version tag" + assert_output --partial "not a branch" +} + +@test "update: DAAF_BRANCH is neither branch nor tag gives generic error" { + setup_state_machine + run bash -c ' + export DAAF_BRANCH="totally-bogus-ref" + docker() { + local all_args="$*" + case "$all_args" in + "info") return 0 ;; + *"compose ps"*"--format"*) echo "daaf-docker" ;; + *"compose exec"*"true"*) return 0 ;; + *"compose exec"*"test -f"*"/daaf/CLAUDE.md"*) return 0 ;; + *"compose exec"*"test -f"*"/daaf/.git/shallow"*) return 1 ;; + *"compose exec"*"remote get-url"*"origin"*) echo "https://github.com/DAAF-Contribution-Community/daaf.git" ;; + *"compose exec"*"fetch"*) return 0 ;; + *"compose exec"*"rev-parse --verify"*"backup/"*) return 1 ;; + *"compose exec"*"rev-parse --verify"*"origin/totally-bogus-ref"*) return 1 ;; + *"compose exec"*"rev-parse --verify"*"refs/tags/totally-bogus-ref"*) return 1 ;; + *"compose exec"*"branch"*) return 0 ;; + *"compose exec"*"rev-parse"*"HEAD"*) echo "abc123" ;; + *) return 0 ;; + esac + } + export -f docker + bash "'"${REPO_ROOT}"'/scripts/host/update_daaf.sh" + ' + assert_failure + assert_output --partial "totally-bogus-ref" + assert_output --partial "was not found" + refute_output --partial "version tag" } diff --git a/tests/powershell/update_daaf.Tests.ps1 b/tests/powershell/update_daaf.Tests.ps1 index 734e477..04a5f36 100644 --- a/tests/powershell/update_daaf.Tests.ps1 +++ b/tests/powershell/update_daaf.Tests.ps1 @@ -716,6 +716,85 @@ function docker { $outputStr = $output | Out-String $LASTEXITCODE | Should -Be 1 $outputStr | Should -BeLike "*was not found*" + $outputStr | Should -Not -BeLike "*version tag*" + Remove-Item Env:DAAF_BRANCH -ErrorAction SilentlyContinue + } + + It "DAAF_BRANCH is a tag gives tag-specific error" { + $env:DAAF_NESTED = "1" + $env:DAAF_BRANCH = "v2.1.0" + $wrapperScript = Join-Path $script:TestDir "test_wrapper_tag.ps1" + Set-Content -Path $wrapperScript -Value @' +$ErrorActionPreference = "Stop" +function docker { + $argStr = $args -join ' ' + $global:LASTEXITCODE = 0 + switch -Wildcard ($argStr) { + "*info*" { return } + "*compose ps*--format*" { Write-Output "daaf-docker" } + "*compose exec*true*" { return } + "*compose exec*test -f*/daaf/.git/shallow*" { $global:LASTEXITCODE = 1; return } + "*compose exec*test -f*" { return } + "*compose exec*git -C /daaf remote get-url origin*" { + Write-Output "https://github.com/DAAF-Contribution-Community/daaf.git" + } + "*compose exec*git -C /daaf fetch*" { return } + "*compose exec*git -C /daaf rev-parse --verify*backup/*" { $global:LASTEXITCODE = 1; return } + "*compose exec*git -C /daaf rev-parse --verify*origin/v2.1.0*" { $global:LASTEXITCODE = 1; return } + "*compose exec*git -C /daaf rev-parse --verify*refs/tags/v2.1.0*" { $global:LASTEXITCODE = 0; return } + "*compose exec*git -C /daaf branch*" { return } + "*compose exec*git -C /daaf rev-parse*HEAD*" { Write-Output "abc123" } + "*compose exec*git*" { return } + "*compose exec*" { return } + default { return } + } +} +'@ + Add-Content -Path $wrapperScript -Value ". '$RepoRoot/scripts/host/update_daaf.ps1'" + $output = & pwsh -NoProfile -File $wrapperScript *>&1 + $outputStr = $output | Out-String + $LASTEXITCODE | Should -Be 1 + $outputStr | Should -BeLike "*version tag*" + $outputStr | Should -BeLike "*not a branch*" + Remove-Item Env:DAAF_BRANCH -ErrorAction SilentlyContinue + } + + It "DAAF_BRANCH is neither branch nor tag gives generic error" { + $env:DAAF_NESTED = "1" + $env:DAAF_BRANCH = "totally-bogus-ref" + $wrapperScript = Join-Path $script:TestDir "test_wrapper_bogus.ps1" + Set-Content -Path $wrapperScript -Value @' +$ErrorActionPreference = "Stop" +function docker { + $argStr = $args -join ' ' + $global:LASTEXITCODE = 0 + switch -Wildcard ($argStr) { + "*info*" { return } + "*compose ps*--format*" { Write-Output "daaf-docker" } + "*compose exec*true*" { return } + "*compose exec*test -f*/daaf/.git/shallow*" { $global:LASTEXITCODE = 1; return } + "*compose exec*test -f*" { return } + "*compose exec*git -C /daaf remote get-url origin*" { + Write-Output "https://github.com/DAAF-Contribution-Community/daaf.git" + } + "*compose exec*git -C /daaf fetch*" { return } + "*compose exec*git -C /daaf rev-parse --verify*backup/*" { $global:LASTEXITCODE = 1; return } + "*compose exec*git -C /daaf rev-parse --verify*origin/totally-bogus-ref*" { $global:LASTEXITCODE = 1; return } + "*compose exec*git -C /daaf rev-parse --verify*refs/tags/totally-bogus-ref*" { $global:LASTEXITCODE = 1; return } + "*compose exec*git -C /daaf branch*" { return } + "*compose exec*git -C /daaf rev-parse*HEAD*" { Write-Output "abc123" } + "*compose exec*git*" { return } + "*compose exec*" { return } + default { return } + } +} +'@ + Add-Content -Path $wrapperScript -Value ". '$RepoRoot/scripts/host/update_daaf.ps1'" + $output = & pwsh -NoProfile -File $wrapperScript *>&1 + $outputStr = $output | Out-String + $LASTEXITCODE | Should -Be 1 + $outputStr | Should -BeLike "*was not found*" + $outputStr | Should -Not -BeLike "*version tag*" Remove-Item Env:DAAF_BRANCH -ErrorAction SilentlyContinue } diff --git a/user_reference/01_installation_and_quickstart.md b/user_reference/01_installation_and_quickstart.md index 455df7e..8f7ac0d 100644 --- a/user_reference/01_installation_and_quickstart.md +++ b/user_reference/01_installation_and_quickstart.md @@ -391,7 +391,7 @@ The update script handles everything for you: - **Syncs utility scripts** — automatically copies any updated host-side scripts (run, backup, rebuild, update) from the container - **Auto-rebuilds if needed** — if the Dockerfile or docker-compose.yml changed, it offers to run `rebuild_daaf` automatically -By default, the update script auto-detects the remote's default branch (`main` or `master`). If you installed from a specific branch (e.g., `dev` or a release tag) and want to keep updating from that branch, set the `DAAF_BRANCH` environment variable — the same one used by the installer: +By default, the update script auto-detects the remote's default branch (`main` or `master`). If you installed from a specific branch (e.g., `dev`) and want to keep updating from that branch, set the `DAAF_BRANCH` environment variable: ```bash # macOS / Linux @@ -403,6 +403,8 @@ $env:DAAF_BRANCH = "dev"; .\update_daaf.ps1 If `DAAF_BRANCH` is not set, the updater defaults to `main` (or `master` if `main` doesn't exist). The script validates that the specified branch exists on the remote before proceeding. +> **Note:** `DAAF_BRANCH` must be a **branch name** for the updater — not a version tag like `v2.1.0`. Tags are fixed snapshots and cannot receive updates. If you installed from a tag, the updater will automatically pull from `main` when `DAAF_BRANCH` is not set. You can use tags with the installer (see "Installing a specific version or branch" below), but for ongoing updates, use the default or specify a branch. + Your research files in `research/` are not tracked by git (they're local to your volume), so they are completely unaffected by updates. **If the Dockerfile changed** (new packages, updated Claude Code version, etc.), you'll also need to rebuild the Docker image. The update script will detect this automatically and print instructions. @@ -486,7 +488,9 @@ $env:DAAF_BRANCH="v2.1.0"; irm "https://raw.githubusercontent.com/DAAF-Contribut $env:DAAF_BRANCH="dev"; irm "https://raw.githubusercontent.com/DAAF-Contribution-Community/daaf/$env:DAAF_BRANCH/scripts/host/install.ps1" | iex ``` -This fetches the installer itself from the specified branch, and also controls the Docker build files and repository clone, so everything comes from the specified branch or tag consistently. The `export` on macOS/Linux is required so that the variable is inherited by the `bash` process on the other side of the pipe. Check the [Releases page](https://github.com/DAAF-Contribution-Community/daaf/releases) to see available versions. If `DAAF_BRANCH` is not set, the installer defaults to `main`. +This fetches the installer itself from the specified branch or tag, and also controls the Docker build files and repository clone, so everything comes from the specified ref consistently. The `export` on macOS/Linux is required so that the variable is inherited by the `bash` process on the other side of the pipe. Check the [Releases page](https://github.com/DAAF-Contribution-Community/daaf/releases) to see available versions. If `DAAF_BRANCH` is not set, the installer defaults to `main`. + +> **Note:** The installer accepts both branch names and version tags, but the **updater** (`update_daaf.sh` / `update_daaf.ps1`) only accepts branch names. If you install from a tag, you do not need to set `DAAF_BRANCH` when updating — the updater will automatically pull from `main`. ### Re-installing DAAF