Workspaces 2/10: standalone persistence and agent recovery #1047
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| # Generated by tend 0.2.5. Regenerate with: uvx tend@latest init | |
| # | |
| # Do not edit this file directly — it will be overwritten on regeneration. | |
| # To customize behavior, edit the relevant skill (for example, | |
| # `running-tend`) in this repo's .claude/skills/ directory, or open an issue at | |
| # https://github.com/max-sixty/tend/issues for changes that need to | |
| # happen upstream in the tend-ci-runner plugin. | |
| name: tend-review | |
| on: | |
| pull_request_target: | |
| types: [opened, synchronize, ready_for_review, reopened] | |
| jobs: | |
| review: | |
| concurrency: | |
| group: ${{ github.workflow }}-${{ github.event.pull_request.number }} | |
| # Preserve pending PR events within GitHub's queue limit: a push must not | |
| # replace a ready-for-review run. Runs stay serial so outward actions | |
| # cannot race. An override setting `cancel-in-progress: true` must set | |
| # `queue: null` too — GitHub rejects the pair as an invalid workflow file. | |
| queue: max | |
| cancel-in-progress: false | |
| runs-on: ubuntu-24.04 | |
| environment: | |
| name: tend | |
| deployment: false | |
| permissions: | |
| contents: write | |
| pull-requests: write | |
| actions: read | |
| issues: write | |
| steps: | |
| - name: Check whether tend is enabled | |
| id: tend_enabled | |
| env: | |
| GH_TOKEN: ${{ github.token }} | |
| run: | | |
| gh api \ | |
| -H "Accept: application/vnd.github.raw+json" \ | |
| "repos/$GITHUB_REPOSITORY/contents/.config/tend.yaml" \ | |
| > "$RUNNER_TEMP/tend.yaml" | |
| ruby - "$RUNNER_TEMP/tend.yaml" <<'RUBY' >> "$GITHUB_OUTPUT" | |
| # Inspect the parsed YAML node so the extra YAML 1.1 boolean words (yes/no/on/off) | |
| # do not diverge from the YAML 1.2 parser used by `tend init`. | |
| require "psych" | |
| path = ARGV.fetch(0) | |
| documents = Psych.parse_stream(File.read(path, mode: "r:bom|utf-8")).children | |
| unless documents.length == 1 | |
| abort "tend config must contain exactly one YAML document" | |
| end | |
| mapping = documents.first.root | |
| unless mapping.is_a?(Psych::Nodes::Mapping) | |
| abort "tend config must contain a YAML mapping" | |
| end | |
| def has_yaml_merge_key?(node) | |
| case node | |
| when Psych::Nodes::Mapping | |
| node.children.each_slice(2).any? do |key, value| | |
| (key.is_a?(Psych::Nodes::Scalar) && key.plain && key.value == "<<") || | |
| has_yaml_merge_key?(key) || has_yaml_merge_key?(value) | |
| end | |
| when Psych::Nodes::Sequence | |
| node.children.any? { |value| has_yaml_merge_key?(value) } | |
| else | |
| false | |
| end | |
| end | |
| if has_yaml_merge_key?(mapping) | |
| abort "tend config: YAML merge keys (<<) are not supported" | |
| end | |
| matches = mapping.children.each_slice(2).select do |key, _value| | |
| key.is_a?(Psych::Nodes::Scalar) && key.value == "enabled" | |
| end | |
| abort "tend config: enabled must appear at most once" if matches.length > 1 | |
| value = matches.dig(0, 1) | |
| enabled = true | |
| if value | |
| bool_tag = value.respond_to?(:tag) && value.tag == "tag:yaml.org,2002:bool" | |
| literal = value.value.downcase if value.is_a?(Psych::Nodes::Scalar) | |
| unless value.is_a?(Psych::Nodes::Scalar) && | |
| (value.plain || bool_tag) && | |
| ["true", "false"].include?(literal) | |
| abort "tend config: enabled must be true or false" | |
| end | |
| enabled = literal == "true" | |
| end | |
| puts "enabled=#{enabled}" | |
| unless enabled | |
| warn "::notice title=Tend disabled::The tend config sets enabled: false; skipping this job" | |
| end | |
| RUBY | |
| - name: React with eyes | |
| if: steps.tend_enabled.outputs.enabled == 'true' | |
| run: | | |
| gh api "repos/$REPO/$TARGET/reactions" -f content=eyes --silent \ | |
| || echo "::warning::could not add the eyes reaction" | |
| env: | |
| REPO: ${{ github.repository }} | |
| TARGET: issues/${{ github.event.pull_request.number }} | |
| GITHUB_TOKEN: ${{ secrets.TEND_BOT_TOKEN }} | |
| # The runner checkout remains on the reviewed base tree for local setup | |
| # actions and their POST chains. The harness creates and selects the PR | |
| # topology in an independent disposable clone. | |
| - uses: actions/checkout@v7 | |
| if: steps.tend_enabled.outputs.enabled == 'true' | |
| with: | |
| fetch-depth: 0 | |
| fetch-tags: true | |
| token: ${{ secrets.TEND_BOT_TOKEN }} | |
| - uses: max-sixty/tend/claude@0.2.5 | |
| if: steps.tend_enabled.outputs.enabled == 'true' | |
| with: | |
| github_token: ${{ secrets.TEND_BOT_TOKEN }} | |
| claude_code_oauth_token: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }} | |
| anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY }} | |
| bot_name: dormouse-bot | |
| model: opus | |
| checkout_mode: review | |
| base_branch: ${{ github.event.pull_request.base.ref }} | |
| prompt: |2 | |
| /tend-ci-runner:review ${{ github.event.pull_request.number }} | |
| - name: Remove the eyes reaction | |
| if: | | |
| always() | |
| && (steps.tend_enabled.outputs.enabled == 'true') | |
| run: | | |
| REACTION_ID=$(gh api --paginate \ | |
| "repos/$REPO/$TARGET/reactions?content=eyes&per_page=100" \ | |
| --jq ".[] | select(.user.login == \"$BOT_NAME\") | .id" | head -n1) | |
| if [ -n "$REACTION_ID" ]; then | |
| gh api -X DELETE "repos/$REPO/$TARGET/reactions/$REACTION_ID" --silent \ | |
| || echo "::warning::could not remove the eyes reaction" | |
| fi | |
| env: | |
| REPO: ${{ github.repository }} | |
| TARGET: issues/${{ github.event.pull_request.number }} | |
| BOT_NAME: dormouse-bot | |
| GITHUB_TOKEN: ${{ secrets.TEND_BOT_TOKEN }} |