From f016f6912544062c01b22d7d53187634b5533a97 Mon Sep 17 00:00:00 2001 From: Andrew Sazonov Date: Tue, 30 Dec 2025 20:53:31 +0100 Subject: [PATCH 1/5] Decouples fit from results; adds show_fit_results --- src/easydiffraction/analysis/analysis.py | 39 ++++++++++++- src/easydiffraction/analysis/fitting.py | 14 +++-- .../easydiffraction/analysis/test_analysis.py | 53 ++++++++++++++++++ .../easydiffraction/analysis/test_fitting.py | 56 +++++++++++++++++++ .../unit/easydiffraction/utils/test_utils.py | 1 + 5 files changed, 158 insertions(+), 5 deletions(-) diff --git a/src/easydiffraction/analysis/analysis.py b/src/easydiffraction/analysis/analysis.py index 404fa835..9e8bd1cd 100644 --- a/src/easydiffraction/analysis/analysis.py +++ b/src/easydiffraction/analysis/analysis.py @@ -501,10 +501,23 @@ def fit(self): """Execute fitting using the selected mode, calculator and minimizer. + This method performs the optimization but does not display + results automatically. Call :meth:`show_fit_results` after + fitting to see a summary of the fit quality and parameter + values. + In 'single' mode, fits each experiment independently. In 'joint' mode, performs a simultaneous fit across experiments with weights. - Sets :attr:`fit_results` on success. + + Sets :attr:`fit_results` on success, which can be accessed + programmatically + (e.g., ``analysis.fit_results.reduced_chi_square``). + + Example:: + + project.analysis.fit() + project.analysis.show_fit_results() # Display results """ sample_models = self.project.sample_models if not sample_models: @@ -554,6 +567,30 @@ def fit(self): # After fitting, get the results self.fit_results = self.fitter.results + def show_fit_results(self) -> None: + """Display a summary of the fit results. + + Renders the fit quality metrics (reduced χ², R-factors) and a + table of fitted parameters with their starting values, final + values, and uncertainties. + + This method should be called after :meth:`fit` completes. If no + fit has been performed yet, a warning is logged. + + Example:: + + project.analysis.fit() + project.analysis.show_fit_results() + """ + if not hasattr(self, 'fit_results') or self.fit_results is None: + log.warning('No fit results available. Run fit() first.') + return + + sample_models = self.project.sample_models + experiments = self.project.experiments + + self.fitter._process_fit_results(sample_models, experiments) + def _update_categories(self, called_by_minimizer=False) -> None: """Update all categories owned by Analysis. diff --git a/src/easydiffraction/analysis/fitting.py b/src/easydiffraction/analysis/fitting.py index 84e60dbf..bf3b851d 100644 --- a/src/easydiffraction/analysis/fitting.py +++ b/src/easydiffraction/analysis/fitting.py @@ -37,6 +37,10 @@ def fit( ) -> None: """Run the fitting process. + This method performs the optimization but does not display + results. Use :meth:`show_fit_results` on the Analysis object + to display the fit results after fitting is complete. + Args: sample_models: Collection of sample models. experiments: Collection of experiments. @@ -66,15 +70,17 @@ def objective_function(engine_params: Dict[str, Any]) -> np.ndarray: # Perform fitting self.results = self.minimizer.fit(params, objective_function) - # Post-fit processing - self._process_fit_results(sample_models, experiments) - def _process_fit_results( self, sample_models: SampleModels, experiments: Experiments, ) -> None: - """Collect reliability inputs and display results after fitting. + """Collect reliability inputs and display fit results. + + This method is typically called by + :meth:`Analysis.show_fit_results` rather than directly. It + calculates R-factors and other metrics, then renders them to + the console. Args: sample_models: Collection of sample models. diff --git a/tests/unit/easydiffraction/analysis/test_analysis.py b/tests/unit/easydiffraction/analysis/test_analysis.py index 53a2b796..b99502e1 100644 --- a/tests/unit/easydiffraction/analysis/test_analysis.py +++ b/tests/unit/easydiffraction/analysis/test_analysis.py @@ -82,3 +82,56 @@ def test_fit_modes_show_and_switch_to_joint(monkeypatch, capsys): out2 = capsys.readouterr().out assert 'Current fit mode changed to' in out2 assert a.fit_mode == 'joint' + + +def test_show_fit_results_warns_when_no_results(capsys): + """Test that show_fit_results logs a warning when fit() has not been run.""" + from easydiffraction.analysis.analysis import Analysis + + a = Analysis(project=_make_project_with_names([])) + + # Ensure fit_results is not set + assert not hasattr(a, 'fit_results') or a.fit_results is None + + a.show_fit_results() + out = capsys.readouterr().out + assert 'No fit results available' in out + + +def test_show_fit_results_calls_process_fit_results(monkeypatch): + """Test that show_fit_results delegates to fitter._process_fit_results.""" + from easydiffraction.analysis.analysis import Analysis + + # Track if _process_fit_results was called + process_called = {'called': False, 'args': None} + + def mock_process_fit_results(sample_models, experiments): + process_called['called'] = True + process_called['args'] = (sample_models, experiments) + + # Create a mock project with sample_models and experiments + class MockProject: + sample_models = object() + experiments = object() + _varname = 'proj' + + class experiments_cls: + names = [] + + experiments = experiments_cls() + + project = MockProject() + project.sample_models = object() + project.experiments.names = [] + + a = Analysis(project=project) + + # Set up fit_results so show_fit_results doesn't return early + a.fit_results = object() + + # Mock the fitter's _process_fit_results method + monkeypatch.setattr(a.fitter, '_process_fit_results', mock_process_fit_results) + + a.show_fit_results() + + assert process_called['called'], '_process_fit_results should be called' diff --git a/tests/unit/easydiffraction/analysis/test_fitting.py b/tests/unit/easydiffraction/analysis/test_fitting.py index 13a173e4..15899c2c 100644 --- a/tests/unit/easydiffraction/analysis/test_fitting.py +++ b/tests/unit/easydiffraction/analysis/test_fitting.py @@ -34,3 +34,59 @@ def fit(self, params, obj): f.fit(sample_models=DummyCollection(), experiments=DummyCollection()) out = capsys.readouterr().out assert 'No parameters selected for fitting' in out + + +def test_fitter_fit_does_not_call_process_fit_results(monkeypatch): + """Test that Fitter.fit() does not automatically call _process_fit_results. + + The display of results is now the responsibility of Analysis.show_fit_results(). + """ + from easydiffraction.analysis.fitting import Fitter + + process_called = {'called': False} + + class DummyParam: + value = 1.0 + _fit_start_value = None + + class DummyCollection: + free_parameters = [DummyParam()] + + def __init__(self): + self._names = ['e1'] + + @property + def names(self): + return self._names + + class MockFitResults: + pass + + class DummyMin: + tracker = type('T', (), {'track': staticmethod(lambda a, b: a)})() + + def fit(self, params, obj): + return MockFitResults() + + def _sync_result_to_parameters(self, params, engine_params): + pass + + f = Fitter() + f.minimizer = DummyMin() + + # Track if _process_fit_results is called + original_process = f._process_fit_results + + def mock_process(*args, **kwargs): + process_called['called'] = True + return original_process(*args, **kwargs) + + monkeypatch.setattr(f, '_process_fit_results', mock_process) + + f.fit(sample_models=DummyCollection(), experiments=DummyCollection()) + + assert not process_called['called'], ( + 'Fitter.fit() should not call _process_fit_results automatically. ' + 'Use Analysis.show_fit_results() instead.' + ) + assert f.results is not None, 'Fitter.fit() should still set results' diff --git a/tests/unit/easydiffraction/utils/test_utils.py b/tests/unit/easydiffraction/utils/test_utils.py index b5fb3e10..278b7918 100644 --- a/tests/unit/easydiffraction/utils/test_utils.py +++ b/tests/unit/easydiffraction/utils/test_utils.py @@ -189,6 +189,7 @@ def test_get_version_for_url_released(monkeypatch): assert MUT._get_version_for_url() == '0.8.0.post1' +@pytest.mark.filterwarnings('ignore:Failed to fetch tutorials index:UserWarning') def test_fetch_tutorials_index_returns_empty_on_error(monkeypatch): import easydiffraction.utils.utils as MUT From 551d8f7f74cfd52796100dbf982591f86fe105fd Mon Sep 17 00:00:00 2001 From: Andrew Sazonov Date: Tue, 30 Dec 2025 20:59:55 +0100 Subject: [PATCH 2/5] Updates tutorials for split fit/results API --- tutorials/ed-1.py | 4 ++++ tutorials/ed-10.py | 1 + tutorials/ed-11.py | 1 + tutorials/ed-12.py | 1 + tutorials/ed-13.py | 5 +++++ tutorials/ed-2.py | 1 + tutorials/ed-3.py | 5 +++++ tutorials/ed-4.py | 1 + tutorials/ed-5.py | 1 + tutorials/ed-6.py | 12 ++++++++++++ tutorials/ed-7.py | 4 ++++ tutorials/ed-8.py | 1 + tutorials/ed-9.py | 1 + 13 files changed, 38 insertions(+) diff --git a/tutorials/ed-1.py b/tutorials/ed-1.py index fb52a375..38bacfda 100644 --- a/tutorials/ed-1.py +++ b/tutorials/ed-1.py @@ -60,6 +60,10 @@ # in the input CIF files, are refined by default. project.analysis.fit() +# %% +# Show fit results summary +project.analysis.show_fit_results() + # %% project.experiments.show_names() diff --git a/tutorials/ed-10.py b/tutorials/ed-10.py index e9c3e796..f3253429 100644 --- a/tutorials/ed-10.py +++ b/tutorials/ed-10.py @@ -83,6 +83,7 @@ # %% project.analysis.current_calculator = 'pdffit' project.analysis.fit() +project.analysis.show_fit_results() # %% [markdown] # ## Plot Measured vs Calculated diff --git a/tutorials/ed-11.py b/tutorials/ed-11.py index 92c378bb..4a5b3176 100644 --- a/tutorials/ed-11.py +++ b/tutorials/ed-11.py @@ -96,6 +96,7 @@ # %% project.analysis.current_calculator = 'pdffit' project.analysis.fit() +project.analysis.show_fit_results() # %% [markdown] # ## Plot Measured vs Calculated diff --git a/tutorials/ed-12.py b/tutorials/ed-12.py index 45e4739d..c40cc5ad 100644 --- a/tutorials/ed-12.py +++ b/tutorials/ed-12.py @@ -117,6 +117,7 @@ # %% project.analysis.current_calculator = 'pdffit' project.analysis.fit() +project.analysis.show_fit_results() # %% [markdown] # ## Plot Measured vs Calculated diff --git a/tutorials/ed-13.py b/tutorials/ed-13.py index fe8efa23..e2bf1110 100644 --- a/tutorials/ed-13.py +++ b/tutorials/ed-13.py @@ -614,6 +614,7 @@ # %% project_1.analysis.fit() +project_1.analysis.show_fit_results() # %% [markdown] # #### Check Fit Results @@ -1109,6 +1110,7 @@ project_2.plot_meas_vs_calc(expt_name='sim_lbco') project_2.analysis.fit() +project_2.analysis.show_fit_results() # %% [markdown] # #### Exercise 5.3: Find the Misfit in the Fit @@ -1177,6 +1179,7 @@ project_2.sample_models['lbco'].cell.length_a.free = True project_2.analysis.fit() +project_2.analysis.show_fit_results() project_2.plot_meas_vs_calc(expt_name='sim_lbco') @@ -1255,6 +1258,7 @@ project_2.experiments['sim_lbco'].peak.asym_alpha_1.free = True project_2.analysis.fit() +project_2.analysis.show_fit_results() project_2.plot_meas_vs_calc(expt_name='sim_lbco', d_spacing=True, x_min=1.35, x_max=1.40) @@ -1422,6 +1426,7 @@ # Now we can perform the fit with both phases included. project_2.analysis.fit() +project_2.analysis.show_fit_results() # Let's plot the measured diffraction pattern and the calculated # diffraction pattern both for the full range and for a zoomed-in region diff --git a/tutorials/ed-2.py b/tutorials/ed-2.py index 92c8bd37..4390b65b 100644 --- a/tutorials/ed-2.py +++ b/tutorials/ed-2.py @@ -160,6 +160,7 @@ # %% project.analysis.fit() +project.analysis.show_fit_results() # %% project.plot_meas_vs_calc(expt_name='hrpt', show_residual=True) diff --git a/tutorials/ed-3.py b/tutorials/ed-3.py index 39df7344..c0aefa1b 100644 --- a/tutorials/ed-3.py +++ b/tutorials/ed-3.py @@ -460,6 +460,7 @@ # %% project.analysis.fit() +project.analysis.show_fit_results() # %% [markdown] # #### Plot Measured vs Calculated @@ -498,6 +499,7 @@ # %% project.analysis.fit() +project.analysis.show_fit_results() # %% [markdown] # #### Plot Measured vs Calculated @@ -536,6 +538,7 @@ # %% project.analysis.fit() +project.analysis.show_fit_results() # %% [markdown] # #### Plot Measured vs Calculated @@ -604,6 +607,7 @@ # %% project.analysis.fit() +project.analysis.show_fit_results() # %% [markdown] # #### Plot Measured vs Calculated @@ -675,6 +679,7 @@ # %% project.analysis.fit() +project.analysis.show_fit_results() # %% [markdown] # #### Plot Measured vs Calculated diff --git a/tutorials/ed-4.py b/tutorials/ed-4.py index a946c659..ce857a2a 100644 --- a/tutorials/ed-4.py +++ b/tutorials/ed-4.py @@ -319,6 +319,7 @@ # %% project.analysis.fit() +project.analysis.show_fit_results() # %% [markdown] # #### Plot Measured vs Calculated diff --git a/tutorials/ed-5.py b/tutorials/ed-5.py index 625ddd0a..0e46baa8 100644 --- a/tutorials/ed-5.py +++ b/tutorials/ed-5.py @@ -288,6 +288,7 @@ # %% project.analysis.fit() +project.analysis.show_fit_results() # %% [markdown] # #### Plot Measured vs Calculated diff --git a/tutorials/ed-6.py b/tutorials/ed-6.py index 77664ba5..106a088c 100644 --- a/tutorials/ed-6.py +++ b/tutorials/ed-6.py @@ -225,6 +225,9 @@ # %% project.analysis.fit() +# %% +project.analysis.show_fit_results() + # %% [markdown] # #### Plot Measured vs Calculated @@ -260,6 +263,9 @@ # %% project.analysis.fit() +# %% +project.analysis.show_fit_results() + # %% [markdown] # #### Plot Measured vs Calculated @@ -293,6 +299,9 @@ # %% project.analysis.fit() +# %% +project.analysis.show_fit_results() + # %% [markdown] # #### Plot Measured vs Calculated @@ -326,6 +335,9 @@ # %% project.analysis.fit() +# %% +project.analysis.show_fit_results() + # %% [markdown] # #### Plot Measured vs Calculated diff --git a/tutorials/ed-7.py b/tutorials/ed-7.py index 26aee1ae..1e12be88 100644 --- a/tutorials/ed-7.py +++ b/tutorials/ed-7.py @@ -178,6 +178,7 @@ # %% project.analysis.fit() +project.analysis.show_fit_results() # %% [markdown] # #### Plot Measured vs Calculated @@ -208,6 +209,7 @@ # %% project.analysis.fit() +project.analysis.show_fit_results() # %% [markdown] # #### Plot Measured vs Calculated @@ -246,6 +248,7 @@ # %% project.analysis.fit() +project.analysis.show_fit_results() # %% [markdown] # #### Plot Measured vs Calculated @@ -275,6 +278,7 @@ # %% project.analysis.fit() +project.analysis.show_fit_results() # %% [markdown] # #### Plot Measured vs Calculated diff --git a/tutorials/ed-8.py b/tutorials/ed-8.py index 4f795376..2aa85227 100644 --- a/tutorials/ed-8.py +++ b/tutorials/ed-8.py @@ -360,6 +360,7 @@ # %% project.analysis.fit() +project.analysis.show_fit_results() # %% [markdown] # #### Plot Measured vs Calculated diff --git a/tutorials/ed-9.py b/tutorials/ed-9.py index 1055a069..a5fe1647 100644 --- a/tutorials/ed-9.py +++ b/tutorials/ed-9.py @@ -309,6 +309,7 @@ # %% project.analysis.fit() +project.analysis.show_fit_results() # %% [markdown] # #### Plot Measured vs Calculated From 48dda4d62f9e2d392d39f8668e6f7cdf5108a399 Mon Sep 17 00:00:00 2001 From: Andrew Sazonov Date: Tue, 30 Dec 2025 21:18:58 +0100 Subject: [PATCH 3/5] Standardizes console separators to em dashes --- src/easydiffraction/utils/logging.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/easydiffraction/utils/logging.py b/src/easydiffraction/utils/logging.py index b56f4a53..890ff134 100644 --- a/src/easydiffraction/utils/logging.py +++ b/src/easydiffraction/utils/logging.py @@ -554,8 +554,8 @@ def paragraph(cls, title: str) -> None: def section(cls, title: str) -> None: """Formats a section header with bold green text.""" full_title = f'{title.upper()}' - line = '━' * len(full_title) - formatted = f'[bold green]\n{line}\n{full_title}\n{line}[/bold green]' + line = '—' * len(full_title) + formatted = f'[bold green]{line}\n{full_title}\n{line}[/bold green]' if not in_jupyter(): formatted = f'\n{formatted}' cls._console.print(formatted) @@ -566,7 +566,7 @@ def chapter(cls, title: str) -> None: and padding. """ width = ConsoleManager._detect_width() - symbol = '─' + symbol = '—' full_title = f' {title.upper()} ' pad_len = (width - len(full_title)) // 2 padding = symbol * pad_len From c6c9efcf13ea1c61b9c3668e27ec7b3adc1f5e89 Mon Sep 17 00:00:00 2001 From: Andrew Sazonov Date: Tue, 30 Dec 2025 22:13:21 +0100 Subject: [PATCH 4/5] Migrates workflows to Easyscience App auth --- .github/workflows/backmerge.yaml | 14 +++++++------- .github/workflows/dashboard.yaml | 10 +++++++++- .github/workflows/docs.yaml | 14 ++++++++++++-- .github/workflows/release-pr.yaml | 22 ++++++++++++++++++---- 4 files changed, 46 insertions(+), 14 deletions(-) diff --git a/.github/workflows/backmerge.yaml b/.github/workflows/backmerge.yaml index 9d3ebe75..f69b5379 100644 --- a/.github/workflows/backmerge.yaml +++ b/.github/workflows/backmerge.yaml @@ -9,8 +9,8 @@ # Required repo config: # https://github.com/organizations/easyscience/settings/secrets/actions # https://github.com/organizations/easyscience/settings/variables/actions -# - Actions secret: BACKMERGE_PRIVATE_KEY (GitHub App private key PEM) -# - Actions variable: BACKMERGE_APP_ID (GitHub App ID) +# - Actions secret: EASYSCIENCE_APP_KEY (GitHub App private key PEM) +# - Actions variable: EASYSCIENCE_APP_ID (GitHub App ID) # The GitHub App must be added to the develop branch ruleset bypass list. name: Backmerge (master -> develop) @@ -31,8 +31,8 @@ jobs: id: app-token uses: actions/create-github-app-token@v2 with: - app-id: ${{ vars.BACKMERGE_APP_ID }} - private-key: ${{ secrets.BACKMERGE_PRIVATE_KEY }} + app-id: ${{ vars.EASYSCIENCE_APP_ID }} + private-key: ${{ secrets.EASYSCIENCE_APP_KEY }} - name: Checkout repository uses: actions/checkout@v5 @@ -40,10 +40,10 @@ jobs: fetch-depth: 0 token: ${{ steps.app-token.outputs.token }} - - name: Configure git + - name: Configure git for pushing run: | - git config user.name "es-backmerge[bot]" - git config user.email "${{ vars.BACKMERGE_APP_ID }}+es-backmerge[bot]@users.noreply.github.com" + git config user.name "easyscience[bot]" + git config user.email "${{ vars.EASYSCIENCE_APP_ID }}+easyscience[bot]@users.noreply.github.com" - name: Merge master into develop run: | diff --git a/.github/workflows/dashboard.yaml b/.github/workflows/dashboard.yaml index 1076f349..a03a370f 100644 --- a/.github/workflows/dashboard.yaml +++ b/.github/workflows/dashboard.yaml @@ -24,6 +24,14 @@ jobs: runs-on: ubuntu-latest steps: + # Create GitHub App token for pushing to external dashboard repo. + - name: Create GitHub App installation token + id: app-token + uses: actions/create-github-app-token@v2 + with: + app-id: ${{ vars.EASYSCIENCE_APP_ID }} + private-key: ${{ secrets.EASYSCIENCE_APP_KEY }} + - name: Checkout repository uses: actions/checkout@v5 with: @@ -80,7 +88,7 @@ jobs: with: external_repository: ${{ env.REPO_OWNER }}/dashboard publish_branch: ${{ env.DEFAULT_BRANCH }} - personal_token: ${{ secrets.GH_API_PERSONAL_ACCESS_TOKEN }} + personal_token: ${{ steps.app-token.outputs.token }} publish_dir: ./_dashboard_publish keep_files: true diff --git a/.github/workflows/docs.yaml b/.github/workflows/docs.yaml index 6f503927..0cf747c3 100644 --- a/.github/workflows/docs.yaml +++ b/.github/workflows/docs.yaml @@ -85,10 +85,20 @@ jobs: echo "DOCS_VERSION=${DOCS_VERSION}" >> "$GITHUB_ENV" echo "DEPLOYMENT_URL=https://easyscience.github.io/${{ github.event.repository.name }}/${DOCS_VERSION}" >> "$GITHUB_ENV" + # Create GitHub App token for pushing to gh-pages as easyscience[bot]. + - name: Create GitHub App installation token + id: app-token + uses: actions/create-github-app-token@v2 + with: + app-id: ${{ vars.EASYSCIENCE_APP_ID }} + private-key: ${{ secrets.EASYSCIENCE_APP_KEY }} + # Check out the repository source code. # Note: The gh-pages branch is fetched separately later for mike deployment. - name: Check-out repository uses: actions/checkout@v5 + with: + token: ${{ steps.app-token.outputs.token }} # Activate dark mode to create documentation with Plotly charts in dark mode # Need a better solution to automatically switch the chart colour theme based on the mkdocs material switcher @@ -179,8 +189,8 @@ jobs: # Configure git user for mike to commit and push to gh-pages branch. - name: Configure git for pushing run: | - git config user.name "github-actions[bot]" - git config user.email "41898282+github-actions[bot]@users.noreply.github.com" + git config user.name "easyscience[bot]" + git config user.email "${{ vars.EASYSCIENCE_APP_ID }}+easyscience[bot]@users.noreply.github.com" # Fetch the gh-pages branch to ensure mike has the latest remote state. # This is required because the checkout step only fetches the source branch, diff --git a/.github/workflows/release-pr.yaml b/.github/workflows/release-pr.yaml index 8f028fd6..eda67884 100644 --- a/.github/workflows/release-pr.yaml +++ b/.github/workflows/release-pr.yaml @@ -1,10 +1,16 @@ -# This workflow creates an automated release PR from `develop` into `main`. +# This workflow creates an automated release PR from `develop` into `master`. # # Usage: # - Triggered manually via workflow_dispatch. # - Creates a PR titled "Release: merge develop into master". # - Adds the label "[maintainer] auto-pull-request" so it is excluded from changelogs. # - The PR body makes clear that this is automation only (no review needed). +# +# Required repo config: +# https://github.com/organizations/easyscience/settings/secrets/actions +# https://github.com/organizations/easyscience/settings/variables/actions +# - Actions secret: EASYSCIENCE_APP_KEY (GitHub App private key PEM) +# - Actions variable: EASYSCIENCE_APP_ID (GitHub App ID) name: Release PR (develop -> master) @@ -13,7 +19,7 @@ on: workflow_dispatch: permissions: - contents: write + contents: read pull-requests: write # Set the environment variables to be used in all jobs defined in this workflow @@ -24,10 +30,18 @@ jobs: create-pull-request: runs-on: ubuntu-latest steps: + - name: Create GitHub App installation token + id: app-token + uses: actions/create-github-app-token@v2 + with: + app-id: ${{ vars.EASYSCIENCE_APP_ID }} + private-key: ${{ secrets.EASYSCIENCE_APP_KEY }} + - name: Checkout develop branch uses: actions/checkout@v5 with: ref: develop + token: ${{ steps.app-token.outputs.token }} - name: Create PR from develop to ${{ env.DEFAULT_BRANCH }} run: | @@ -36,8 +50,8 @@ jobs: --head develop \ --title "Release: merge develop into ${{ env.DEFAULT_BRANCH }}" \ --label "[maintainer] auto-pull-request" \ - --body "⚠️ This PR is created automatically to trigger the release pipeline. It merges the accumulated changes from \`develop\` into \`${{ env.DEFAULT_BRANCH }}\`. + --body "This PR is created automatically to trigger the release pipeline. It merges the accumulated changes from \`develop\` into \`${{ env.DEFAULT_BRANCH }}\`. It is labeled \`[maintainer] auto-pull-request\` and is excluded from release notes and version bump logic." env: - GITHUB_TOKEN: ${{ secrets.GH_API_PERSONAL_ACCESS_TOKEN }} + GH_TOKEN: ${{ steps.app-token.outputs.token }} From 9787ce217c67e2ad6c5fe8efb1496df0911b0aa8 Mon Sep 17 00:00:00 2001 From: Andrew Sazonov Date: Tue, 30 Dec 2025 22:22:42 +0100 Subject: [PATCH 5/5] Grants app token access to dashboard repo --- .github/workflows/dashboard.yaml | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/.github/workflows/dashboard.yaml b/.github/workflows/dashboard.yaml index a03a370f..93445c3f 100644 --- a/.github/workflows/dashboard.yaml +++ b/.github/workflows/dashboard.yaml @@ -25,12 +25,17 @@ jobs: steps: # Create GitHub App token for pushing to external dashboard repo. + # The 'repositories' parameter is required to grant access to repos + # other than the one where the workflow is running. - name: Create GitHub App installation token id: app-token uses: actions/create-github-app-token@v2 with: app-id: ${{ vars.EASYSCIENCE_APP_ID }} private-key: ${{ secrets.EASYSCIENCE_APP_KEY }} + repositories: | + ${{ github.event.repository.name }} + dashboard - name: Checkout repository uses: actions/checkout@v5