@elalfner/relevo 0.9.1 → 0.9.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "relevo",
3
3
  "displayName": "Relevo",
4
- "version": "0.9.1",
4
+ "version": "0.9.3",
5
5
  "description": "An automatic issue-to-merge workflow where an agent builds and people verify.",
6
6
  "author": {
7
7
  "name": "Elias Alfaro"
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@elalfner/relevo",
3
- "version": "0.9.1",
3
+ "version": "0.9.3",
4
4
  "description": "Relevo: an automatic issue-to-merge workflow where an agent builds and people verify.",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -670,7 +670,7 @@ export function runChecks(f) {
670
670
  if (f.protectionError) {
671
671
  r.push(warn('repo.protection', `could not read the branch rules (${f.protectionError}), so they were not checked`, 'run doctor again; if it keeps failing, check your access to the repository'));
672
672
  } else if (f.protectionUnavailable) {
673
- r.push(warn('repo.protection', `branch protection is not available on this repository (${f.protectionUnavailable})`, 'make the repository public or upgrade the plan; until then approval and merge are only enforced by the written confirmation in `ship`'));
673
+ r.push(note('repo.protection', `no branch protection is available on this plan (${f.protectionUnavailable}): approval and merge are only enforced by the written confirmation in \`ship\`; making the repository public or upgrading the plan would add it`));
674
674
  } else {
675
675
  [['base', f.config?.branches?.base ?? 'base'], ['release', f.config?.branches?.release ?? 'release']].forEach(([key, name]) => {
676
676
  const p = f.protection?.[key];
@@ -767,7 +767,7 @@ function shipChecks(f) {
767
767
 
768
768
  // The action that sets up the toolchain of a stack in the build job of the go workflow. Vue needs only Node, which the
769
769
  // job installs for Relevo itself, and a stack that is not listed here is not judged.
770
- const TOOLCHAIN_ACTIONS = { flutter: { action: 'subosito/flutter-action', step: 'the Flutter step' }, laravel: { action: 'shivammathur/setup-php', step: 'the Laravel step' } };
770
+ const TOOLCHAIN_ACTIONS = { flutter: { action: 'subosito/flutter-action', step: 'the Flutter step' }, laravel: { action: 'shivammathur/setup-php', step: 'the Laravel step' }, supabase: { action: 'supabase/setup-cli', step: 'the Supabase step' } };
771
771
 
772
772
  /**
773
773
  * Does the go workflow install the toolchain of each configured platform? Without it every cloud build fails QA after
@@ -334,7 +334,7 @@ export function planInit({ facts, answers, templates, current, admin, upgrades =
334
334
  items.push(item(`workflow.${file}`, 'differs', `.github/workflows/${file} differs from the template and could not be checked for an update (${up.message}); it is left as it is`));
335
335
  } else {
336
336
  // The cloud build only passes QA when the build job installs the toolchain of the stack, which the template does.
337
- const toolchain = file === 'relevo-go.yml' ? '; check that its build job installs the toolchain of your stacks, as the template does for Flutter and Laravel' : '';
337
+ const toolchain = file === 'relevo-go.yml' ? '; check that its build job installs the toolchain of your stacks, as the template does for Flutter, Laravel and Supabase' : '';
338
338
  items.push(item(`workflow.${file}`, 'differs', `.github/workflows/${file} exists and differs from the template; it is left as it is (compare it by hand${toolchain})`));
339
339
  }
340
340
  });
@@ -35,7 +35,17 @@ export function loadPr(gh, repo, branch) {
35
35
  return open ? JSON.parse(gh(['pr', 'view', String(open.number), '--repo', repo, '--json', PR_FIELDS])) : null;
36
36
  }
37
37
 
38
- const notFound = (err) => /404|Not Found/i.test(String(err?.stderr ?? err?.message ?? err));
38
+ const errorText = (err) => `${err?.stderr ?? ''}\n${err?.message ?? err}`;
39
+ const notFound = (err) => /404|Not Found/i.test(errorText(err));
40
+
41
+ /**
42
+ * True when GitHub refuses a branch-rules call because of the plan or the visibility of the repository (a private
43
+ * repository on the free plan has no rulesets and no protection), not because of the token or a fault of its own.
44
+ */
45
+ export function planRefusal(err) {
46
+ const text = errorText(err);
47
+ return /HTTP 403/.test(text) && /Upgrade to GitHub|make this repository public/i.test(text);
48
+ }
39
49
 
40
50
  /** True when every file that changed from `baseline` to `head` is under `openspec/`, and something changed. */
41
51
  export function changedOnlySpecs(gh, repo, baseline, head) {
@@ -62,16 +72,22 @@ export function baselineChecks(gh, repo, baseline, head) {
62
72
 
63
73
  /**
64
74
  * Whether merging into `branch` requires status checks, by a ruleset or by classic branch protection.
65
- * Returns { required: boolean, contexts: [names] }. A branch without protection requires none.
75
+ * Returns { required: boolean, contexts: [names] }. A branch without protection requires none, and neither does one
76
+ * whose rules the plan of the repository does not offer. Any other failure to read them is thrown: checks that exist
77
+ * must not be skipped because they could not be read.
66
78
  */
67
79
  export function requiredChecks(gh, repo, branch) {
68
80
  const contexts = new Set();
69
81
  let required = false;
70
- const rules = JSON.parse(gh(['api', `repos/${repo}/rules/branches/${encodeURIComponent(branch)}`]));
71
- (Array.isArray(rules) ? rules : []).filter((r) => r.type === 'required_status_checks').forEach((r) => {
72
- required = true;
73
- (r.parameters?.required_status_checks ?? []).forEach((c) => contexts.add(c.context));
74
- });
82
+ try {
83
+ const rules = JSON.parse(gh(['api', `repos/${repo}/rules/branches/${encodeURIComponent(branch)}`]));
84
+ (Array.isArray(rules) ? rules : []).filter((r) => r.type === 'required_status_checks').forEach((r) => {
85
+ required = true;
86
+ (r.parameters?.required_status_checks ?? []).forEach((c) => contexts.add(c.context));
87
+ });
88
+ } catch (err) {
89
+ if (!planRefusal(err)) throw err;
90
+ }
75
91
  try {
76
92
  const protection = JSON.parse(gh(['api', `repos/${repo}/branches/${encodeURIComponent(branch)}/protection`]));
77
93
  const classic = protection.required_status_checks;
@@ -81,7 +97,7 @@ export function requiredChecks(gh, repo, branch) {
81
97
  (classic.checks ?? []).forEach((c) => contexts.add(c.context));
82
98
  }
83
99
  } catch (err) {
84
- if (!notFound(err)) throw err;
100
+ if (!notFound(err) && !planRefusal(err)) throw err;
85
101
  }
86
102
  return { required, contexts: [...contexts] };
87
103
  }
@@ -262,8 +262,9 @@ export async function runUnattendedShip(ctx) {
262
262
  }
263
263
  }
264
264
  return { outcome: 'merged', pr: pr.number, commit };
265
- } catch (err) {
266
- if (!(err instanceof Stop)) throw err;
265
+ } catch (caught) {
266
+ // An error the run did not foresee (a refusal of GitHub, for example) stops it like any other reason, with its message.
267
+ const err = caught instanceof Stop ? caught : new Stop('ship_stopped', { reasons: `unexpected error: ${firstLines(caught?.stderr || caught?.message || caught)}` });
267
268
  removeShipLabel();
268
269
  gh(['issue', 'comment', n, '--repo', repo, '--body', renderMessage(err.kind, { actor, issue: n, ...err.data }, config.language)]);
269
270
  return { outcome: 'stopped', kind: err.kind, ...err.data };
@@ -34,7 +34,7 @@ If `relevo.yml` already exists and is valid, skip this step. Otherwise ask, in s
34
34
  - who builds and reviews (GitHub usernames) and who writes the specs;
35
35
  - the review mode: `none` (a solo project), `peer` or `roles`;
36
36
  - the language of the hand-off messages (`en` or `es`);
37
- - the platforms: for each one a name, its stack (look at the project to propose one: the profiles in `${CLAUDE_PLUGIN_ROOT}/stacks/` are the stacks Relevo ships with: `flutter`, `vue`, `laravel` and `supabase`), the folder where it lives (`.` for a repository with one platform, `apps/web` in a monorepo) and, for a stack that has targets, which ones (for Flutter: `android`, `ios`, `web`; for Vue: `web`; Laravel and Supabase have none). Several platforms in one repository are written down, but `go` builds only a repository with one for now;
37
+ - the platforms: for each one a name, its stack (look at the project to propose one: the profiles in `${CLAUDE_PLUGIN_ROOT}/stacks/` are the stacks Relevo ships with: `flutter`, `vue`, `laravel` and `supabase`), the folder where it lives (`.` for a repository with one platform, `apps/web` in a monorepo) and, for a stack that has targets, which ones (for Flutter: `android`, `ios`, `web`; for Vue: `web`; Laravel and Supabase have none). Several platforms in one repository are supported: each issue names the platform it is for with a `Platform:` line, and `go` builds that one;
38
38
  - where `go` builds: in the cloud, on each person's machine, or both (the default), and, for the cloud, with an API key from the Anthropic Console (the default) or with a subscription token. A subscription's quota is shared by every run and by the interactive use of the account, so `init` sets `limits.concurrent_agents` to 1 for it. Nothing about this goes in `relevo.yml` apart from that number: the installed workflows, the secret that exists and the repository variable `RELEVO_CLOUD_GO` (`off` pauses the cloud at once, with no pull request) decide it. Whatever the answer, the ship workflow (`relevo-ship.yml`) is added: it ships an approved issue when a person applies the `relevo:ship` label, runs no model and needs no model credential, and the repository variable `RELEVO_CLOUD_SHIP` (`off`) pauses it;
39
39
  - whether the repository runs its GitHub Actions on a self-hosted runner. Ask nothing more when the answer is no: the workflows run on `ubuntu-latest` and `relevo.yml` gets no `runner` key. On a yes, ask for the runner's labels (for example `self-hosted` and `linux-build`): they become `runner.labels` in `relevo.yml` and the `runs-on` of every workflow `init` adds. Say that Relevo assumes a self-hosted runner keeps its files between jobs, whatever the person says about it, and that the agent's code runs with the permissions of the runner's user; do not ask whether the runner is ephemeral;
40
40
  - the repositories this one depends on, if any (`owner/repo`, for example the back end of a front end): they are written to `links`, the closed list that `Depends-on: owner/repo#N` is checked against;
@@ -1,4 +1,4 @@
1
- # relevo-template: 0.9.1
1
+ # relevo-template: 0.9.3
2
2
  # Relevo: posts one digest per working day, as a comment on the issue named by digest.issue in relevo.yml.
3
3
  # It lists who is waiting on what and the alerts, and posts nothing when nothing is waiting.
4
4
  # Subscribe your chat tool to that issue to receive it. Copy to .github/workflows/relevo-digest.yml.
@@ -14,7 +14,7 @@ name: relevo-digest
14
14
  # The version of @elalfner/relevo whose scripts this workflow runs. The plugin's templates carry the version they
15
15
  # were released with, and `/relevo:init` reports this file as different when it is older; change it by hand to update.
16
16
  env:
17
- RELEVO_VERSION: '0.9.1'
17
+ RELEVO_VERSION: '0.9.3'
18
18
 
19
19
  on:
20
20
  schedule:
@@ -1,4 +1,4 @@
1
- # relevo-template: 0.9.1
1
+ # relevo-template: 0.9.3
2
2
  # Relevo: builds an issue when someone with write access labels it `relevo:ready`.
3
3
  # Copy to .github/workflows/relevo-go.yml. If you renamed the ready label in relevo.yml, change it below too.
4
4
  #
@@ -20,7 +20,7 @@ name: relevo-go
20
20
  # The version of @elalfner/relevo whose scripts this workflow runs. The plugin's templates carry the version they
21
21
  # were released with, and `/relevo:init` reports this file as different when it is older; change it by hand to update.
22
22
  env:
23
- RELEVO_VERSION: '0.9.1'
23
+ RELEVO_VERSION: '0.9.3'
24
24
  # The version of OpenSpec that the job installs for itself.
25
25
  OPENSPEC_VERSION: '1.14.3'
26
26
 
@@ -143,8 +143,8 @@ jobs:
143
143
  # issue is installed here. The `decide` job tells which one (`needs.decide.outputs.stack`) and where it lives
144
144
  # (`needs.decide.outputs.path`), so an issue for the front does not install the back's tools: a step whose stack
145
145
  # is not the issue's is skipped and costs nothing. The names are the `stack` of relevo.yml. Change the versions
146
- # and the caching to suit the project; `/relevo:doctor` fails when a Flutter or Laravel platform has no active
147
- # step below, because every build would fail QA after spending the run.
146
+ # and the caching to suit the project; `/relevo:doctor` fails when a Flutter, Laravel or Supabase platform has no
147
+ # active step below, because every build would fail QA after spending the run.
148
148
  - if: needs.decide.outputs.stack == 'flutter'
149
149
  uses: subosito/flutter-action@v2
150
150
  with:
@@ -155,6 +155,11 @@ jobs:
155
155
  with:
156
156
  php-version: '8.3'
157
157
  tools: composer
158
+ # Supabase: the CLI runs the checks of the profile against containers on the runner, which already has Docker.
159
+ - if: needs.decide.outputs.stack == 'supabase'
160
+ uses: supabase/setup-cli@v1
161
+ with:
162
+ version: latest
158
163
  # Vue needs only Node, which this job installed above; to cache the packages, uncomment:
159
164
  # - if: needs.decide.outputs.stack == 'vue'
160
165
  # uses: actions/setup-node@v4
@@ -1,4 +1,4 @@
1
- # relevo-template: 0.9.1
1
+ # relevo-template: 0.9.3
2
2
  # Relevo: re-evaluates the queue and starts the issues that are now eligible.
3
3
  # Copy to .github/workflows/relevo-queue.yml next to relevo-go.yml (it starts builds through that workflow).
4
4
  #
@@ -15,7 +15,7 @@ name: relevo-queue
15
15
  # The version of @elalfner/relevo whose scripts this workflow runs. The plugin's templates carry the version they
16
16
  # were released with, and `/relevo:init` reports this file as different when it is older; change it by hand to update.
17
17
  env:
18
- RELEVO_VERSION: '0.9.1'
18
+ RELEVO_VERSION: '0.9.3'
19
19
 
20
20
  on:
21
21
  issues:
@@ -1,4 +1,4 @@
1
- # relevo-template: 0.9.1
1
+ # relevo-template: 0.9.3
2
2
  # Relevo: keeps the issue label in step with its pull request.
3
3
  # A review that requests changes sets `relevo:changes-requested` and mentions the author; a new push returns the
4
4
  # issue to `relevo:pr-open`; an approval mentions the person who will run /relevo:ship.
@@ -15,7 +15,7 @@ name: relevo-review
15
15
  # The version of @elalfner/relevo whose scripts this workflow runs. The plugin's templates carry the version they
16
16
  # were released with, and `/relevo:init` reports this file as different when it is older; change it by hand to update.
17
17
  env:
18
- RELEVO_VERSION: '0.9.1'
18
+ RELEVO_VERSION: '0.9.3'
19
19
 
20
20
  on:
21
21
  pull_request_review:
@@ -1,4 +1,4 @@
1
- # relevo-template: 0.9.1
1
+ # relevo-template: 0.9.3
2
2
  # Relevo: ships an issue when a person applies the `relevo:ship` label to it.
3
3
  # Copy to .github/workflows/relevo-ship.yml. If you renamed the ship label in relevo.yml, change it below too.
4
4
  #
@@ -27,7 +27,7 @@ name: relevo-ship
27
27
  # The version of @elalfner/relevo whose scripts this workflow runs. The plugin's templates carry the version they
28
28
  # were released with, and `/relevo:init` reports this file as different when it is older; change it by hand to update.
29
29
  env:
30
- RELEVO_VERSION: '0.9.1'
30
+ RELEVO_VERSION: '0.9.3'
31
31
  # The version of OpenSpec that the job installs for itself.
32
32
  OPENSPEC_VERSION: '1.14.3'
33
33
 
@@ -44,6 +44,10 @@ permissions:
44
44
  contents: write
45
45
  issues: write
46
46
  pull-requests: write
47
+ # To read the checks of the approved head (the status check rollup of a commit); without them the run stops on
48
+ # "Resource not accessible by integration".
49
+ checks: read
50
+ statuses: read
47
51
  # To start the queue (relevo-queue.yml) after the close, which the workflow token's close does not start by itself.
48
52
  actions: write
49
53
 
@@ -1,4 +1,4 @@
1
- # relevo-template: 0.9.1
1
+ # relevo-template: 0.9.3
2
2
  # Relevo: reminds the reviewer when a pull request waits longer than limits.review_sla_hours, and escalates
3
3
  # to the spec writer (adding the needs-reviewer label) after the same time again.
4
4
  # Copy to .github/workflows/relevo-sla.yml. Needs a relevo.yml at the repository root.
@@ -13,7 +13,7 @@ name: relevo-sla
13
13
  # The version of @elalfner/relevo whose scripts this workflow runs. The plugin's templates carry the version they
14
14
  # were released with, and `/relevo:init` reports this file as different when it is older; change it by hand to update.
15
15
  env:
16
- RELEVO_VERSION: '0.9.1'
16
+ RELEVO_VERSION: '0.9.3'
17
17
 
18
18
  on:
19
19
  schedule: