From 1e7fabdd345c1f283b53abd3c04c0b49e5f10ce4 Mon Sep 17 00:00:00 2001 From: Miranda Mundt Date: Mon, 17 Jun 2024 10:32:32 -0600 Subject: [PATCH 1/7] Update Pyomo for NumPy 2 --- pyomo/common/dependencies.py | 10 ++++++++-- pyomo/core/kernel/register_numpy_types.py | 10 ++++++---- .../tests/unit/test_kernel_register_numpy_types.py | 6 ++++-- pyomo/core/tests/unit/test_numvalue.py | 8 ++++---- pyomo/core/tests/unit/test_sets.py | 12 ++++++------ 5 files changed, 28 insertions(+), 18 deletions(-) diff --git a/pyomo/common/dependencies.py b/pyomo/common/dependencies.py index 4c9e43002ef..cf05b68612d 100644 --- a/pyomo/common/dependencies.py +++ b/pyomo/common/dependencies.py @@ -999,10 +999,13 @@ def _finalize_numpy(np, available): # registration here (to bypass the deprecation warning) until we # finally remove all support for it numeric_types._native_boolean_types.add(t) - _floats = [np.float_, np.float16, np.float32, np.float64] + _floats = [np.float16, np.float32, np.float64] # float96 and float128 may or may not be defined in this particular # numpy build (it depends on platform and version). # Register them only if they are present + if hasattr(np, 'float_'): + # Prepend to preserve previous functionality + _floats = [np.float_] + _floats if hasattr(np, 'float96'): _floats.append(np.float96) if hasattr(np, 'float128'): @@ -1013,10 +1016,13 @@ def _finalize_numpy(np, available): # registration here (to bypass the deprecation warning) until we # finally remove all support for it numeric_types._native_boolean_types.add(t) - _complex = [np.complex_, np.complex64, np.complex128] + _complex = [np.complex64, np.complex128] # complex192 and complex256 may or may not be defined in this # particular numpy build (it depends on platform and version). # Register them only if they are present + if hasattr(np, 'np.complex_'): + # Prepend to preserve functionality + _complex = [np.complex_] + _complex if hasattr(np, 'complex192'): _complex.append(np.complex192) if hasattr(np, 'complex256'): diff --git a/pyomo/core/kernel/register_numpy_types.py b/pyomo/core/kernel/register_numpy_types.py index 86877be2230..b3405645d97 100644 --- a/pyomo/core/kernel/register_numpy_types.py +++ b/pyomo/core/kernel/register_numpy_types.py @@ -45,10 +45,12 @@ # Historically, the lists included several numpy aliases numpy_int_names.extend(('int_', 'intc', 'intp')) numpy_int.extend((numpy.int_, numpy.intc, numpy.intp)) - numpy_float_names.append('float_') - numpy_float.append(numpy.float_) - numpy_complex_names.append('complex_') - numpy_complex.append(numpy.complex_) + if hasattr(numpy, 'float_'): + numpy_float_names.append('float_') + numpy_float.append(numpy.float_) + if hasattr(numpy, 'complex_'): + numpy_complex_names.append('complex_') + numpy_complex.append(numpy.complex_) # Re-build the old numpy_* lists for t in native_boolean_types: diff --git a/pyomo/core/tests/unit/test_kernel_register_numpy_types.py b/pyomo/core/tests/unit/test_kernel_register_numpy_types.py index 91a0f571881..d49b3cd6b4a 100644 --- a/pyomo/core/tests/unit/test_kernel_register_numpy_types.py +++ b/pyomo/core/tests/unit/test_kernel_register_numpy_types.py @@ -34,7 +34,8 @@ # Reals numpy_float_names = [] if numpy_available: - numpy_float_names.append('float_') + if hasattr(numpy, 'float_'): + numpy_float_names.append('float_') numpy_float_names.append('float16') numpy_float_names.append('float32') numpy_float_names.append('float64') @@ -46,7 +47,8 @@ # Complex numpy_complex_names = [] if numpy_available: - numpy_complex_names.append('complex_') + if hasattr(numpy, 'complex_'): + numpy_complex_names.append('complex_') numpy_complex_names.append('complex64') numpy_complex_names.append('complex128') if hasattr(numpy, 'complex192'): diff --git a/pyomo/core/tests/unit/test_numvalue.py b/pyomo/core/tests/unit/test_numvalue.py index 1cccd3863ea..4d39a42ed70 100644 --- a/pyomo/core/tests/unit/test_numvalue.py +++ b/pyomo/core/tests/unit/test_numvalue.py @@ -552,10 +552,10 @@ def test_unknownNumericType(self): @unittest.skipUnless(numpy_available, "This test requires NumPy") def test_numpy_basic_float_registration(self): - self.assertIn(numpy.float_, native_numeric_types) - self.assertNotIn(numpy.float_, native_integer_types) - self.assertIn(numpy.float_, _native_boolean_types) - self.assertIn(numpy.float_, native_types) + self.assertIn(numpy.float64, native_numeric_types) + self.assertNotIn(numpy.float64, native_integer_types) + self.assertIn(numpy.float64, _native_boolean_types) + self.assertIn(numpy.float64, native_types) @unittest.skipUnless(numpy_available, "This test requires NumPy") def test_numpy_basic_int_registration(self): diff --git a/pyomo/core/tests/unit/test_sets.py b/pyomo/core/tests/unit/test_sets.py index 48869397aae..4d305ebab86 100644 --- a/pyomo/core/tests/unit/test_sets.py +++ b/pyomo/core/tests/unit/test_sets.py @@ -1051,7 +1051,7 @@ def setUp(self): self.instance = self.model.create_instance(currdir + "setA.dat") self.e1 = numpy.bool_(1) self.e2 = numpy.int_(2) - self.e3 = numpy.float_(3.0) + self.e3 = numpy.float64(3.0) self.e4 = numpy.int_(4) self.e5 = numpy.int_(5) self.e6 = numpy.int_(6) @@ -1068,7 +1068,7 @@ def test_numpy_int(self): def test_numpy_float(self): model = ConcreteModel() - model.A = Set(initialize=[numpy.float_(1.0), numpy.float_(0.0)]) + model.A = Set(initialize=[numpy.float64(1.0), numpy.float64(0.0)]) self.assertEqual(model.A.bounds(), (0, 1)) @@ -3213,7 +3213,7 @@ def test_numpy_membership(self): self.assertEqual(numpy.int_(1) in Boolean, True) self.assertEqual(numpy.bool_(True) in Boolean, True) self.assertEqual(numpy.bool_(False) in Boolean, True) - self.assertEqual(numpy.float_(1.1) in Boolean, False) + self.assertEqual(numpy.float64(1.1) in Boolean, False) self.assertEqual(numpy.int_(2) in Boolean, False) self.assertEqual(numpy.int_(0) in Integers, True) @@ -3222,7 +3222,7 @@ def test_numpy_membership(self): # identically to 1 self.assertEqual(numpy.bool_(True) in Integers, True) self.assertEqual(numpy.bool_(False) in Integers, True) - self.assertEqual(numpy.float_(1.1) in Integers, False) + self.assertEqual(numpy.float64(1.1) in Integers, False) self.assertEqual(numpy.int_(2) in Integers, True) self.assertEqual(numpy.int_(0) in Reals, True) @@ -3231,14 +3231,14 @@ def test_numpy_membership(self): # identically to 1 self.assertEqual(numpy.bool_(True) in Reals, True) self.assertEqual(numpy.bool_(False) in Reals, True) - self.assertEqual(numpy.float_(1.1) in Reals, True) + self.assertEqual(numpy.float64(1.1) in Reals, True) self.assertEqual(numpy.int_(2) in Reals, True) self.assertEqual(numpy.int_(0) in Any, True) self.assertEqual(numpy.int_(1) in Any, True) self.assertEqual(numpy.bool_(True) in Any, True) self.assertEqual(numpy.bool_(False) in Any, True) - self.assertEqual(numpy.float_(1.1) in Any, True) + self.assertEqual(numpy.float64(1.1) in Any, True) self.assertEqual(numpy.int_(2) in Any, True) def test_setargs1(self): From cb6723041d999380ef4452158b9dce99629020c0 Mon Sep 17 00:00:00 2001 From: Miranda Mundt Date: Mon, 17 Jun 2024 12:50:10 -0600 Subject: [PATCH 2/7] Change to insert --- pyomo/common/dependencies.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pyomo/common/dependencies.py b/pyomo/common/dependencies.py index cf05b68612d..bbcea0b85d7 100644 --- a/pyomo/common/dependencies.py +++ b/pyomo/common/dependencies.py @@ -1005,7 +1005,7 @@ def _finalize_numpy(np, available): # Register them only if they are present if hasattr(np, 'float_'): # Prepend to preserve previous functionality - _floats = [np.float_] + _floats + _floats.insert(0, np.float_) if hasattr(np, 'float96'): _floats.append(np.float96) if hasattr(np, 'float128'): @@ -1022,7 +1022,7 @@ def _finalize_numpy(np, available): # Register them only if they are present if hasattr(np, 'np.complex_'): # Prepend to preserve functionality - _complex = [np.complex_] + _complex + _complex.insert(0, np.complex_) if hasattr(np, 'complex192'): _complex.append(np.complex192) if hasattr(np, 'complex256'): From 6f86e3231f0c82e4a42693030fb64ba1f58b51dc Mon Sep 17 00:00:00 2001 From: John Siirola Date: Mon, 24 Jun 2024 10:15:53 -0600 Subject: [PATCH 3/7] Switch writers to use str() (instead of repr()) for numeric constants In Python 3.x, str() and repr() are the same for float, and beginning in numpy 2.0 `repr(float64(n))` produces `numpy.float64(n)`, whereas str() produces `n` --- pyomo/repn/plugins/lp_writer.py | 24 ++++++++++++------------ pyomo/repn/plugins/nl_writer.py | 28 ++++++++++++++-------------- 2 files changed, 26 insertions(+), 26 deletions(-) diff --git a/pyomo/repn/plugins/lp_writer.py b/pyomo/repn/plugins/lp_writer.py index 627a54e3f68..814f79a4eb9 100644 --- a/pyomo/repn/plugins/lp_writer.py +++ b/pyomo/repn/plugins/lp_writer.py @@ -458,13 +458,13 @@ def write(self, model): addSymbol(con, label) ostream.write(f'\n{label}:\n') self.write_expression(ostream, repn, False) - ostream.write(f'>= {(lb - offset)!r}\n') + ostream.write(f'>= {(lb - offset)!s}\n') elif lb == ub: label = f'c_e_{symbol}_' addSymbol(con, label) ostream.write(f'\n{label}:\n') self.write_expression(ostream, repn, False) - ostream.write(f'= {(lb - offset)!r}\n') + ostream.write(f'= {(lb - offset)!s}\n') else: # We will need the constraint body twice. Generate # in a buffer so we only have to do that once. @@ -476,18 +476,18 @@ def write(self, model): addSymbol(con, label) ostream.write(f'\n{label}:\n') ostream.write(buf) - ostream.write(f'>= {(lb - offset)!r}\n') + ostream.write(f'>= {(lb - offset)!s}\n') label = f'r_u_{symbol}_' aliasSymbol(con, label) ostream.write(f'\n{label}:\n') ostream.write(buf) - ostream.write(f'<= {(ub - offset)!r}\n') + ostream.write(f'<= {(ub - offset)!s}\n') elif ub is not None: label = f'c_u_{symbol}_' addSymbol(con, label) ostream.write(f'\n{label}:\n') self.write_expression(ostream, repn, False) - ostream.write(f'<= {(ub - offset)!r}\n') + ostream.write(f'<= {(ub - offset)!s}\n') if with_debug_timing: # report the last constraint @@ -527,8 +527,8 @@ def write(self, model): # Note: Var.bounds guarantees the values are either (finite) # native_numeric_types or None lb, ub = v.bounds - lb = '-inf' if lb is None else repr(lb) - ub = '+inf' if ub is None else repr(ub) + lb = '-inf' if lb is None else str(lb) + ub = '+inf' if ub is None else str(ub) ostream.write(f"\n {lb} <= {v_symbol} <= {ub}") if integer_vars: @@ -565,7 +565,7 @@ def write(self, model): for v, w in getattr(soscon, 'get_items', soscon.items)(): if w.__class__ not in int_float: w = float(f) - ostream.write(f" {getSymbol(v)}:{w!r}\n") + ostream.write(f" {getSymbol(v)}:{w!s}\n") ostream.write("\nend\n") @@ -584,9 +584,9 @@ def write_expression(self, ostream, expr, is_objective): expr.linear.items(), key=lambda x: getVarOrder(x[0]) ): if coef < 0: - ostream.write(f'{coef!r} {getSymbol(getVar(vid))}\n') + ostream.write(f'{coef!s} {getSymbol(getVar(vid))}\n') else: - ostream.write(f'+{coef!r} {getSymbol(getVar(vid))}\n') + ostream.write(f'+{coef!s} {getSymbol(getVar(vid))}\n') quadratic = getattr(expr, 'quadratic', None) if quadratic: @@ -605,9 +605,9 @@ def _normalize_constraint(data): col = c1, c2 sym = f' {getSymbol(getVar(vid1))} * {getSymbol(getVar(vid2))}\n' if coef < 0: - return col, repr(coef) + sym + return col, str(coef) + sym else: - return col, '+' + repr(coef) + sym + return col, f'+{coef!s}{sym}' if is_objective: # diff --git a/pyomo/repn/plugins/nl_writer.py b/pyomo/repn/plugins/nl_writer.py index 43fd2fade68..a8966e44f71 100644 --- a/pyomo/repn/plugins/nl_writer.py +++ b/pyomo/repn/plugins/nl_writer.py @@ -1150,17 +1150,17 @@ def write(self, model): r_lines[idx] = "3" else: # _type = 4 # L == c == U - r_lines[idx] = f"4 {lb - expr_info.const!r}" + r_lines[idx] = f"4 {lb - expr_info.const!s}" n_equality += 1 elif lb is None: # _type = 1 # c <= U - r_lines[idx] = f"1 {ub - expr_info.const!r}" + r_lines[idx] = f"1 {ub - expr_info.const!s}" elif ub is None: # _type = 2 # L <= c - r_lines[idx] = f"2 {lb - expr_info.const!r}" + r_lines[idx] = f"2 {lb - expr_info.const!s}" else: # _type = 0 # L <= c <= U - r_lines[idx] = f"0 {lb - expr_info.const!r} {ub - expr_info.const!r}" + r_lines[idx] = f"0 {lb - expr_info.const!s} {ub - expr_info.const!s}" n_ranges += 1 expr_info.const = 0 # FIXME: this is a HACK to be compatible with the NLv1 @@ -1375,7 +1375,7 @@ def write(self, model): ostream.write(f"S{_field|_float} {len(_vals)} {name}\n") # Note: _SuffixData.compile() guarantees the value is int/float ostream.write( - ''.join(f"{_id} {_vals[_id]!r}\n" for _id in sorted(_vals)) + ''.join(f"{_id} {_vals[_id]!s}\n" for _id in sorted(_vals)) ) # @@ -1485,7 +1485,7 @@ def write(self, model): ostream.write(f"d{len(data.con)}\n") # Note: _SuffixData.compile() guarantees the value is int/float ostream.write( - ''.join(f"{_id} {data.con[_id]!r}\n" for _id in sorted(data.con)) + ''.join(f"{_id} {data.con[_id]!s}\n" for _id in sorted(data.con)) ) # @@ -1507,7 +1507,7 @@ def write(self, model): ) ostream.write( ''.join( - f'{var_idx} {val!r}{col_comments[var_idx]}\n' + f'{var_idx} {val!s}{col_comments[var_idx]}\n' for var_idx, val in _init_lines ) ) @@ -1548,13 +1548,13 @@ def write(self, model): if lb is None: # unbounded ostream.write(f"3{col_comments[var_idx]}\n") else: # == - ostream.write(f"4 {lb!r}{col_comments[var_idx]}\n") + ostream.write(f"4 {lb!s}{col_comments[var_idx]}\n") elif lb is None: # var <= ub - ostream.write(f"1 {ub!r}{col_comments[var_idx]}\n") + ostream.write(f"1 {ub!s}{col_comments[var_idx]}\n") elif ub is None: # lb <= body - ostream.write(f"2 {lb!r}{col_comments[var_idx]}\n") + ostream.write(f"2 {lb!s}{col_comments[var_idx]}\n") else: # lb <= body <= ub - ostream.write(f"0 {lb!r} {ub!r}{col_comments[var_idx]}\n") + ostream.write(f"0 {lb!s} {ub!s}{col_comments[var_idx]}\n") # # "k" lines (column offsets in Jacobian NNZ) @@ -1589,7 +1589,7 @@ def write(self, model): linear[_id] /= scaling_cache[_id] ostream.write(f'J{row_idx} {len(linear)}{row_comments[row_idx]}\n') for _id in sorted(linear, key=column_order.__getitem__): - ostream.write(f'{column_order[_id]} {linear[_id]!r}\n') + ostream.write(f'{column_order[_id]} {linear[_id]!s}\n') # # "G" lines (non-empty terms in the Objective) @@ -1605,7 +1605,7 @@ def write(self, model): linear[_id] /= scaling_cache[_id] ostream.write(f'G{obj_idx} {len(linear)}{row_comments[obj_idx + n_cons]}\n') for _id in sorted(linear, key=column_order.__getitem__): - ostream.write(f'{column_order[_id]} {linear[_id]!r}\n') + ostream.write(f'{column_order[_id]} {linear[_id]!s}\n') # Generate the return information eliminated_vars = [ @@ -2041,7 +2041,7 @@ def _write_v_line(self, expr_id, k): # ostream.write(f'V{self.next_V_line_id} {len(linear)} {k}{lbl}\n') for _id in sorted(linear, key=column_order.__getitem__): - ostream.write(f'{column_order[_id]} {linear[_id]!r}\n') + ostream.write(f'{column_order[_id]} {linear[_id]!s}\n') self._write_nl_expression(info[1], True) self.next_V_line_id += 1 From 2145a1684c726d8170460ef148be673459c3eac5 Mon Sep 17 00:00:00 2001 From: John Siirola Date: Mon, 24 Jun 2024 11:06:37 -0600 Subject: [PATCH 4/7] Improve tokenization in baseline comparisons --- pyomo/common/unittest.py | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/pyomo/common/unittest.py b/pyomo/common/unittest.py index 84d962eb784..a947420a25d 100644 --- a/pyomo/common/unittest.py +++ b/pyomo/common/unittest.py @@ -807,6 +807,15 @@ def filter_file_contents(self, lines, abstol=None): item_list = [] items = line.strip().split() for i in items: + # Split up lists, dicts, and sets + while i and i[0] in '[{': + item_list.append(i[0]) + i = i[1:] + tail = [] + while i and i[-1] in ',:]}': + tail.append(i[-1]) + i = i[:-1] + # A few substitutions to get tests passing on pypy3 if ".inf" in i: i = i.replace(".inf", "inf") @@ -817,6 +826,9 @@ def filter_file_contents(self, lines, abstol=None): item_list.append(float(i)) except: item_list.append(i) + if tail: + tail.reverse() + item_list.extend(tail) # We can get printed results objects where the baseline is # exactly 0 (and omitted) and the test is slightly non-zero. From e5973d4014a63fe7093b3ea70292b14650f9f06f Mon Sep 17 00:00:00 2001 From: John Siirola Date: Mon, 24 Jun 2024 11:07:06 -0600 Subject: [PATCH 5/7] Map new numpy type representations back to pre-numpy2.0 behavior --- pyomo/common/unittest.py | 10 +++++++++- .../tests/unit/test_kernel_register_numpy_types.py | 5 ++++- 2 files changed, 13 insertions(+), 2 deletions(-) diff --git a/pyomo/common/unittest.py b/pyomo/common/unittest.py index a947420a25d..5b6d1f003d8 100644 --- a/pyomo/common/unittest.py +++ b/pyomo/common/unittest.py @@ -783,6 +783,7 @@ def filter_fcn(self, line): return False def filter_file_contents(self, lines, abstol=None): + _numpy_scalar_re = re.compile(r'np.(int|float)\d+\(([^\)]+)\)') filtered = [] deprecated = None for line in lines: @@ -823,7 +824,14 @@ def filter_file_contents(self, lines, abstol=None): i = i.replace("null", "None") try: - item_list.append(float(i)) + # Numpy 2.x changed the repr for scalars. Convert + # the new scalar reprs back to the original (which + # were indistinguishable from python floats/ints) + np_match = _numpy_scalar_re.match(i) + if np_match: + item_list.append(float(np_match.group(2))) + else: + item_list.append(float(i)) except: item_list.append(i) if tail: diff --git a/pyomo/core/tests/unit/test_kernel_register_numpy_types.py b/pyomo/core/tests/unit/test_kernel_register_numpy_types.py index d49b3cd6b4a..8186c3d6028 100644 --- a/pyomo/core/tests/unit/test_kernel_register_numpy_types.py +++ b/pyomo/core/tests/unit/test_kernel_register_numpy_types.py @@ -16,7 +16,10 @@ # Boolean numpy_bool_names = [] if numpy_available: - numpy_bool_names.append('bool_') + if numpy.__version__[0] == '2': + numpy_bool_names.append('bool') + else: + numpy_bool_names.append('bool_') # Integers numpy_int_names = [] if numpy_available: From c88e0d5ad9130e24d8019052001196afe0da2222 Mon Sep 17 00:00:00 2001 From: John Siirola Date: Mon, 24 Jun 2024 13:03:57 -0600 Subject: [PATCH 6/7] Update near-0 filter based on new tokenizer --- pyomo/common/unittest.py | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/pyomo/common/unittest.py b/pyomo/common/unittest.py index 5b6d1f003d8..c78e003a07d 100644 --- a/pyomo/common/unittest.py +++ b/pyomo/common/unittest.py @@ -844,12 +844,13 @@ def filter_file_contents(self, lines, abstol=None): # results objects and remote them if they are within # tolerance of 0 if ( - len(item_list) == 2 - and item_list[0] == 'Value:' - and type(item_list[1]) is float - and abs(item_list[1]) < (abstol or 0) - and len(filtered[-1]) == 1 - and filtered[-1][0][-1] == ':' + len(item_list) == 3 + and item_list[0] == 'Value' + and item_list[1] == ':' + and type(item_list[2]) is float + and abs(item_list[2]) < (abstol or 0) + and len(filtered[-1]) == 2 + and filtered[-1][1] == ':' ): filtered.pop() else: From 3ea998aa71f297016fdee8541f232ed8746aaeff Mon Sep 17 00:00:00 2001 From: Miranda Mundt Date: Mon, 24 Jun 2024 14:17:54 -0600 Subject: [PATCH 7/7] Sneak in fix for codecov tokenless upload --- .github/workflows/test_branches.yml | 6 ++++++ .github/workflows/test_pr_and_main.yml | 6 ++++++ 2 files changed, 12 insertions(+) diff --git a/.github/workflows/test_branches.yml b/.github/workflows/test_branches.yml index 8ba04eec466..2689b97746b 100644 --- a/.github/workflows/test_branches.yml +++ b/.github/workflows/test_branches.yml @@ -855,6 +855,9 @@ jobs: token: ${{ secrets.PYOMO_CODECOV_TOKEN }} name: ${{ matrix.TARGET }} flags: ${{ matrix.TARGET }} + # downgrading after v0.7.0 broke tokenless upload + # see codecov/codecov-action#1487 + version: v0.6.0 fail_ci_if_error: true - name: Upload other coverage reports @@ -867,4 +870,7 @@ jobs: token: ${{ secrets.PYOMO_CODECOV_TOKEN }} name: ${{ matrix.TARGET }}/other flags: ${{ matrix.TARGET }},other + # downgrading after v0.7.0 broke tokenless upload + # see codecov/codecov-action#1487 + version: v0.6.0 fail_ci_if_error: true diff --git a/.github/workflows/test_pr_and_main.yml b/.github/workflows/test_pr_and_main.yml index bdf1f7e1aa5..c804372b18a 100644 --- a/.github/workflows/test_pr_and_main.yml +++ b/.github/workflows/test_pr_and_main.yml @@ -899,6 +899,9 @@ jobs: token: ${{ secrets.PYOMO_CODECOV_TOKEN }} name: ${{ matrix.TARGET }} flags: ${{ matrix.TARGET }} + # downgrading after v0.7.0 broke tokenless upload + # see codecov/codecov-action#1487 + version: v0.6.0 fail_ci_if_error: true - name: Upload other coverage reports @@ -911,4 +914,7 @@ jobs: token: ${{ secrets.PYOMO_CODECOV_TOKEN }} name: ${{ matrix.TARGET }}/other flags: ${{ matrix.TARGET }},other + # downgrading after v0.7.0 broke tokenless upload + # see codecov/codecov-action#1487 + version: v0.6.0 fail_ci_if_error: true